@zag-js/date-picker
Version:
Core logic for the date-picker widget implemented as a state machine
1,382 lines (1,380 loc) • 72.5 kB
JavaScript
import { createAnatomy } from '@zag-js/anatomy';
import { DateFormatter, isEqualDay, CalendarDate, parseDate, isWeekend, isToday } from '@internationalized/date';
import { getEndDate, isDateOutsideRange, alignDate, getDecadeRange, getTodayDate, getPreviousSection, getNextSection, getPreviousPage, getNextPage, isDateEqual, constrainValue, formatSelectedDate, isNextRangeInvalid, isPreviousRangeInvalid, parseDateString, getAdjustedDateFn, getWeekDays, getDateRangePreset, getMonthFormatter, getDaysInWeek, getMonthDays, getMonthNames, getYearsRange, isDateUnavailable, getDayFormatter, getUnitDuration } from '@zag-js/date-utils';
import { queryAll, setElementValue, raf, query, restoreTextSelection, disableTextSelection, getNativeEvent, dataAttr, ariaAttr, getEventKey, isComposingEvent } from '@zag-js/dom-query';
import { getPlacement, getPlacementStyles } from '@zag-js/popper';
import { createSplitProps, clampValue, chunk, isValueWithinRange, match } from '@zag-js/utils';
import { createGuards, createMachine } from '@zag-js/core';
import { trackDismissableElement } from '@zag-js/dismissable';
import { createLiveRegion } from '@zag-js/live-region';
import { createProps } from '@zag-js/types';
// src/date-picker.anatomy.ts
var anatomy = createAnatomy("date-picker").parts(
"clearTrigger",
"content",
"control",
"input",
"label",
"monthSelect",
"nextTrigger",
"positioner",
"presetTrigger",
"prevTrigger",
"rangeText",
"root",
"table",
"tableBody",
"tableCell",
"tableCellTrigger",
"tableHead",
"tableHeader",
"tableRow",
"trigger",
"view",
"viewControl",
"viewTrigger",
"yearSelect"
);
var parts = anatomy.build();
var getLabelId = (ctx, index) => ctx.ids?.label?.(index) ?? `datepicker:${ctx.id}:label:${index}`;
var getRootId = (ctx) => ctx.ids?.root ?? `datepicker:${ctx.id}`;
var getTableId = (ctx, id) => ctx.ids?.table?.(id) ?? `datepicker:${ctx.id}:table:${id}`;
var getContentId = (ctx) => ctx.ids?.content ?? `datepicker:${ctx.id}:content`;
var getCellTriggerId = (ctx, id) => ctx.ids?.cellTrigger?.(id) ?? `datepicker:${ctx.id}:cell-trigger:${id}`;
var getPrevTriggerId = (ctx, view) => ctx.ids?.prevTrigger?.(view) ?? `datepicker:${ctx.id}:prev:${view}`;
var getNextTriggerId = (ctx, view) => ctx.ids?.nextTrigger?.(view) ?? `datepicker:${ctx.id}:next:${view}`;
var getViewTriggerId = (ctx, view) => ctx.ids?.viewTrigger?.(view) ?? `datepicker:${ctx.id}:view:${view}`;
var getClearTriggerId = (ctx) => ctx.ids?.clearTrigger ?? `datepicker:${ctx.id}:clear`;
var getControlId = (ctx) => ctx.ids?.control ?? `datepicker:${ctx.id}:control`;
var getInputId = (ctx, index) => ctx.ids?.input?.(index) ?? `datepicker:${ctx.id}:input:${index}`;
var getTriggerId = (ctx) => ctx.ids?.trigger ?? `datepicker:${ctx.id}:trigger`;
var getPositionerId = (ctx) => ctx.ids?.positioner ?? `datepicker:${ctx.id}:positioner`;
var getMonthSelectId = (ctx) => ctx.ids?.monthSelect ?? `datepicker:${ctx.id}:month-select`;
var getYearSelectId = (ctx) => ctx.ids?.yearSelect ?? `datepicker:${ctx.id}:year-select`;
var getFocusedCell = (ctx, view) => query(getContentEl(ctx), `[data-part=table-cell-trigger][data-view=${view}][data-focus]:not([data-outside-range])`);
var getTriggerEl = (ctx) => ctx.getById(getTriggerId(ctx));
var getContentEl = (ctx) => ctx.getById(getContentId(ctx));
var getInputEls = (ctx) => queryAll(getControlEl(ctx), `[data-part=input]`);
var getYearSelectEl = (ctx) => ctx.getById(getYearSelectId(ctx));
var getMonthSelectEl = (ctx) => ctx.getById(getMonthSelectId(ctx));
var getClearTriggerEl = (ctx) => ctx.getById(getClearTriggerId(ctx));
var getPositionerEl = (ctx) => ctx.getById(getPositionerId(ctx));
var getControlEl = (ctx) => ctx.getById(getControlId(ctx));
function adjustStartAndEndDate(value) {
const [startDate, endDate] = value;
if (!startDate || !endDate) return value;
return startDate.compare(endDate) <= 0 ? value : [endDate, startDate];
}
function isDateWithinRange(date, value) {
const [startDate, endDate] = value;
if (!startDate || !endDate) return false;
return startDate.compare(date) <= 0 && endDate.compare(date) >= 0;
}
function sortDates(values) {
return values.slice().sort((a, b) => a.compare(b));
}
function getRoleDescription(view) {
return match(view, {
year: "calendar decade",
month: "calendar year",
day: "calendar month"
});
}
var PLACEHOLDERS = {
day: "dd",
month: "mm",
year: "yyyy"
};
function getInputPlaceholder(locale) {
return new DateFormatter(locale).formatToParts(/* @__PURE__ */ new Date()).map((item) => PLACEHOLDERS[item.type] ?? item.value).join("");
}
var isValidCharacter = (char, separator) => {
if (!char) return true;
return /\d/.test(char) || char === separator || char.length !== 1;
};
var isValidDate = (value) => {
return !Number.isNaN(value.day) && !Number.isNaN(value.month) && !Number.isNaN(value.year);
};
var ensureValidCharacters = (value, separator) => {
return value.split("").filter((char) => isValidCharacter(char, separator)).join("");
};
function getLocaleSeparator(locale) {
const dateFormatter = new Intl.DateTimeFormat(locale);
const parts2 = dateFormatter.formatToParts(/* @__PURE__ */ new Date());
const literalPart = parts2.find((part) => part.type === "literal");
return literalPart ? literalPart.value : "/";
}
var defaultTranslations = {
dayCell(state) {
if (state.unavailable) return `Not available. ${state.formattedDate}`;
if (state.selected) return `Selected date. ${state.formattedDate}`;
return `Choose ${state.formattedDate}`;
},
trigger(open) {
return open ? "Close calendar" : "Open calendar";
},
viewTrigger(view) {
return match(view, {
year: "Switch to month view",
month: "Switch to day view",
day: "Switch to year view"
});
},
presetTrigger(value) {
return Array.isArray(value) ? `select ${value[0].toString()} to ${value[1].toString()}` : `select ${value}`;
},
prevTrigger(view) {
return match(view, {
year: "Switch to previous decade",
month: "Switch to previous year",
day: "Switch to previous month"
});
},
nextTrigger(view) {
return match(view, {
year: "Switch to next decade",
month: "Switch to next year",
day: "Switch to next month"
});
},
// TODO: Revisit this
placeholder() {
return { day: "dd", month: "mm", year: "yyyy" };
},
content: "calendar",
monthSelect: "Select month",
yearSelect: "Select year",
clearTrigger: "Clear selected dates"
};
function viewToNumber(view, fallback) {
if (!view) return fallback || 0;
return view === "day" ? 0 : view === "month" ? 1 : 2;
}
function viewNumberToView(viewNumber) {
return viewNumber === 0 ? "day" : viewNumber === 1 ? "month" : "year";
}
function clampView(view, minView, maxView) {
return viewNumberToView(
clampValue(viewToNumber(view, 0), viewToNumber(minView, 0), viewToNumber(maxView, 2))
);
}
function isAboveMinView(view, minView) {
return viewToNumber(view, 0) > viewToNumber(minView, 0);
}
function isBelowMinView(view, minView) {
return viewToNumber(view, 0) < viewToNumber(minView, 0);
}
function getNextView(view, minView, maxView) {
const nextViewNumber = viewToNumber(view, 0) + 1;
return clampView(viewNumberToView(nextViewNumber), minView, maxView);
}
function getPreviousView(view, minView, maxView) {
const prevViewNumber = viewToNumber(view, 0) - 1;
return clampView(viewNumberToView(prevViewNumber), minView, maxView);
}
var views = ["day", "month", "year"];
function eachView(cb) {
views.forEach((view) => cb(view));
}
// src/date-picker.connect.ts
function connect(service, normalize) {
const { state, context, prop, send, computed, scope } = service;
const startValue = context.get("startValue");
const endValue = computed("endValue");
const selectedValue = context.get("value");
const focusedValue = context.get("focusedValue");
const hoveredValue = context.get("hoveredValue");
const hoveredRangeValue = hoveredValue ? adjustStartAndEndDate([selectedValue[0], hoveredValue]) : [];
const disabled = prop("disabled");
const readOnly = prop("readOnly");
const interactive = computed("isInteractive");
const min = prop("min");
const max = prop("max");
const locale = prop("locale");
const timeZone = prop("timeZone");
const startOfWeek = prop("startOfWeek");
const focused = state.matches("focused");
const open = state.matches("open");
const isRangePicker = prop("selectionMode") === "range";
const isDateUnavailableFn = prop("isDateUnavailable");
const currentPlacement = context.get("currentPlacement");
const popperStyles = getPlacementStyles({
...prop("positioning"),
placement: currentPlacement
});
const separator = getLocaleSeparator(locale);
const translations = { ...defaultTranslations, ...prop("translations") };
function getMonthWeeks(from = startValue) {
const numOfWeeks = prop("fixedWeeks") ? 6 : void 0;
return getMonthDays(from, locale, numOfWeeks, startOfWeek);
}
function getMonths(props2 = {}) {
const { format } = props2;
return getMonthNames(locale, format).map((label, index) => ({ label, value: index + 1 }));
}
function getYears() {
const range = getYearsRange({ from: min?.year ?? 1900, to: max?.year ?? 2100 });
return range.map((year) => ({ label: year.toString(), value: year }));
}
function getDecadeYears(year) {
const range = getDecadeRange(focusedValue.year);
return range.map((year2) => ({ label: year2.toString(), value: year2 }));
}
function isUnavailable(date) {
return isDateUnavailable(date, isDateUnavailableFn, locale, min, max);
}
function focusMonth(month) {
const date = startValue ?? getTodayDate(timeZone);
send({ type: "FOCUS.SET", value: date.set({ month }) });
}
function focusYear(year) {
const date = startValue ?? getTodayDate(timeZone);
send({ type: "FOCUS.SET", value: date.set({ year }) });
}
function getYearTableCellState(props2) {
const { value, disabled: disabled2 } = props2;
const dateValue = focusedValue.set({ year: value });
const cellState = {
focused: focusedValue.year === props2.value,
selectable: isValueWithinRange(value, min?.year ?? 0, max?.year ?? 9999),
selected: !!selectedValue.find((date) => date.year === value),
valueText: value.toString(),
inRange: isRangePicker && (isDateWithinRange(dateValue, selectedValue) || isDateWithinRange(dateValue, hoveredRangeValue)),
value: dateValue,
get disabled() {
return disabled2 || !cellState.selectable;
}
};
return cellState;
}
function getMonthTableCellState(props2) {
const { value, disabled: disabled2 } = props2;
const dateValue = focusedValue.set({ month: value });
const formatter = getMonthFormatter(locale, timeZone);
const cellState = {
focused: focusedValue.month === props2.value,
selectable: !isDateOutsideRange(dateValue, min, max),
selected: !!selectedValue.find((date) => date.month === value && date.year === focusedValue.year),
valueText: formatter.format(dateValue.toDate(timeZone)),
inRange: isRangePicker && (isDateWithinRange(dateValue, selectedValue) || isDateWithinRange(dateValue, hoveredRangeValue)),
value: dateValue,
get disabled() {
return disabled2 || !cellState.selectable;
}
};
return cellState;
}
function getDayTableCellState(props2) {
const { value, disabled: disabled2, visibleRange = computed("visibleRange") } = props2;
const formatter = getDayFormatter(locale, timeZone);
const unitDuration = getUnitDuration(computed("visibleDuration"));
const outsideDaySelectable = prop("outsideDaySelectable");
const end = visibleRange.start.add(unitDuration).subtract({ days: 1 });
const isOutsideRange = isDateOutsideRange(value, visibleRange.start, end);
const isInSelectedRange = isRangePicker && isDateWithinRange(value, selectedValue);
const isFirstInSelectedRange = isRangePicker && isDateEqual(value, selectedValue[0]);
const isLastInSelectedRange = isRangePicker && isDateEqual(value, selectedValue[1]);
const hasHoveredRange = isRangePicker && hoveredRangeValue.length > 0;
const isInHoveredRange = hasHoveredRange && isDateWithinRange(value, hoveredRangeValue);
const isFirstInHoveredRange = hasHoveredRange && isDateEqual(value, hoveredRangeValue[0]);
const isLastInHoveredRange = hasHoveredRange && isDateEqual(value, hoveredRangeValue[1]);
const cellState = {
invalid: isDateOutsideRange(value, min, max),
disabled: disabled2 || !outsideDaySelectable && isOutsideRange || isDateOutsideRange(value, min, max),
selected: selectedValue.some((date) => isDateEqual(value, date)),
unavailable: isDateUnavailable(value, isDateUnavailableFn, locale, min, max) && !disabled2,
outsideRange: isOutsideRange,
today: isToday(value, timeZone),
weekend: isWeekend(value, locale),
formattedDate: formatter.format(value.toDate(timeZone)),
get focused() {
return isDateEqual(value, focusedValue) && (!cellState.outsideRange || outsideDaySelectable);
},
get ariaLabel() {
return translations.dayCell(cellState);
},
get selectable() {
return !cellState.disabled && !cellState.unavailable;
},
// Range states
inRange: isInSelectedRange || isInHoveredRange,
firstInRange: isFirstInSelectedRange,
lastInRange: isLastInSelectedRange,
// Preview range states
inHoveredRange: isInHoveredRange,
firstInHoveredRange: isFirstInHoveredRange,
lastInHoveredRange: isLastInHoveredRange
};
return cellState;
}
function getTableId2(props2) {
const { view = "day", id } = props2;
return [view, id].filter(Boolean).join(" ");
}
return {
focused,
open,
inline: !!prop("inline"),
view: context.get("view"),
getRangePresetValue(preset) {
return getDateRangePreset(preset, locale, timeZone);
},
getDaysInWeek(week, from = startValue) {
return getDaysInWeek(week, from, locale, startOfWeek);
},
getOffset(duration) {
const from = startValue.add(duration);
const end = endValue.add(duration);
const formatter = getMonthFormatter(locale, timeZone);
return {
visibleRange: { start: from, end },
weeks: getMonthWeeks(from),
visibleRangeText: {
start: formatter.format(from.toDate(timeZone)),
end: formatter.format(end.toDate(timeZone))
}
};
},
getMonthWeeks,
isUnavailable,
weeks: getMonthWeeks(),
weekDays: getWeekDays(getTodayDate(timeZone), startOfWeek, timeZone, locale),
visibleRangeText: computed("visibleRangeText"),
value: selectedValue,
valueAsDate: selectedValue.map((date) => date.toDate(timeZone)),
valueAsString: computed("valueAsString"),
focusedValue,
focusedValueAsDate: focusedValue?.toDate(timeZone),
focusedValueAsString: prop("format")(focusedValue, { locale, timeZone }),
visibleRange: computed("visibleRange"),
selectToday() {
const value = constrainValue(getTodayDate(timeZone), min, max);
send({ type: "VALUE.SET", value });
},
setValue(values) {
const computedValue = values.map((date) => constrainValue(date, min, max));
send({ type: "VALUE.SET", value: computedValue });
},
clearValue() {
send({ type: "VALUE.CLEAR" });
},
setFocusedValue(value) {
send({ type: "FOCUS.SET", value });
},
setOpen(nextOpen) {
if (prop("inline")) return;
const open2 = state.matches("open");
if (open2 === nextOpen) return;
send({ type: nextOpen ? "OPEN" : "CLOSE" });
},
focusMonth,
focusYear,
getYears,
getMonths,
getYearsGrid(props2 = {}) {
const { columns = 1 } = props2;
return chunk(getDecadeYears(), columns);
},
getDecade() {
const years = getDecadeRange(focusedValue.year);
return { start: years.at(0), end: years.at(-1) };
},
getMonthsGrid(props2 = {}) {
const { columns = 1, format } = props2;
return chunk(getMonths({ format }), columns);
},
format(value, opts = { month: "long", year: "numeric" }) {
return new DateFormatter(locale, opts).format(value.toDate(timeZone));
},
setView(view) {
send({ type: "VIEW.SET", view });
},
goToNext() {
send({ type: "GOTO.NEXT", view: context.get("view") });
},
goToPrev() {
send({ type: "GOTO.PREV", view: context.get("view") });
},
getRootProps() {
return normalize.element({
...parts.root.attrs,
dir: prop("dir"),
id: getRootId(scope),
"data-state": open ? "open" : "closed",
"data-disabled": dataAttr(disabled),
"data-readonly": dataAttr(readOnly)
});
},
getLabelProps(props2 = {}) {
const { index = 0 } = props2;
return normalize.label({
...parts.label.attrs,
id: getLabelId(scope, index),
dir: prop("dir"),
htmlFor: getInputId(scope, index),
"data-state": open ? "open" : "closed",
"data-index": index,
"data-disabled": dataAttr(disabled),
"data-readonly": dataAttr(readOnly)
});
},
getControlProps() {
return normalize.element({
...parts.control.attrs,
dir: prop("dir"),
id: getControlId(scope),
"data-disabled": dataAttr(disabled)
});
},
getRangeTextProps() {
return normalize.element({
...parts.rangeText.attrs,
dir: prop("dir")
});
},
getContentProps() {
return normalize.element({
...parts.content.attrs,
hidden: !open,
dir: prop("dir"),
"data-state": open ? "open" : "closed",
"data-placement": currentPlacement,
id: getContentId(scope),
tabIndex: -1,
role: "application",
"aria-roledescription": "datepicker",
"aria-label": translations.content
});
},
getTableProps(props2 = {}) {
const { view = "day", columns = view === "day" ? 7 : 4 } = props2;
const uid = getTableId2(props2);
return normalize.element({
...parts.table.attrs,
role: "grid",
"data-columns": columns,
"aria-roledescription": getRoleDescription(view),
id: getTableId(scope, uid),
"aria-readonly": ariaAttr(readOnly),
"aria-disabled": ariaAttr(disabled),
"aria-multiselectable": ariaAttr(prop("selectionMode") !== "single"),
"data-view": view,
dir: prop("dir"),
tabIndex: -1,
onKeyDown(event) {
if (event.defaultPrevented) return;
const keyMap = {
Enter() {
if (view === "day" && isUnavailable(focusedValue)) return;
if (view === "month") {
const cellState = getMonthTableCellState({ value: focusedValue.month });
if (!cellState.selectable) return;
}
if (view === "year") {
const cellState = getYearTableCellState({ value: focusedValue.year });
if (!cellState.selectable) return;
}
send({ type: "TABLE.ENTER", view, columns, focus: true });
},
ArrowLeft() {
send({ type: "TABLE.ARROW_LEFT", view, columns, focus: true });
},
ArrowRight() {
send({ type: "TABLE.ARROW_RIGHT", view, columns, focus: true });
},
ArrowUp() {
send({ type: "TABLE.ARROW_UP", view, columns, focus: true });
},
ArrowDown() {
send({ type: "TABLE.ARROW_DOWN", view, columns, focus: true });
},
PageUp(event2) {
send({ type: "TABLE.PAGE_UP", larger: event2.shiftKey, view, columns, focus: true });
},
PageDown(event2) {
send({ type: "TABLE.PAGE_DOWN", larger: event2.shiftKey, view, columns, focus: true });
},
Home() {
send({ type: "TABLE.HOME", view, columns, focus: true });
},
End() {
send({ type: "TABLE.END", view, columns, focus: true });
}
};
const exec = keyMap[getEventKey(event, {
dir: prop("dir")
})];
if (exec) {
exec(event);
event.preventDefault();
event.stopPropagation();
}
},
onPointerLeave() {
send({ type: "TABLE.POINTER_LEAVE" });
},
onPointerDown() {
send({ type: "TABLE.POINTER_DOWN", view });
},
onPointerUp() {
send({ type: "TABLE.POINTER_UP", view });
}
});
},
getTableHeadProps(props2 = {}) {
const { view = "day" } = props2;
return normalize.element({
...parts.tableHead.attrs,
"aria-hidden": true,
dir: prop("dir"),
"data-view": view,
"data-disabled": dataAttr(disabled)
});
},
getTableHeaderProps(props2 = {}) {
const { view = "day" } = props2;
return normalize.element({
...parts.tableHeader.attrs,
dir: prop("dir"),
"data-view": view,
"data-disabled": dataAttr(disabled)
});
},
getTableBodyProps(props2 = {}) {
const { view = "day" } = props2;
return normalize.element({
...parts.tableBody.attrs,
"data-view": view,
"data-disabled": dataAttr(disabled)
});
},
getTableRowProps(props2 = {}) {
const { view = "day" } = props2;
return normalize.element({
...parts.tableRow.attrs,
"aria-disabled": ariaAttr(disabled),
"data-disabled": dataAttr(disabled),
"data-view": view
});
},
getDayTableCellState,
getDayTableCellProps(props2) {
const { value } = props2;
const cellState = getDayTableCellState(props2);
return normalize.element({
...parts.tableCell.attrs,
role: "gridcell",
"aria-disabled": ariaAttr(!cellState.selectable),
"aria-selected": cellState.selected || cellState.inRange,
"aria-invalid": ariaAttr(cellState.invalid),
"aria-current": cellState.today ? "date" : void 0,
"data-value": value.toString()
});
},
getDayTableCellTriggerProps(props2) {
const { value } = props2;
const cellState = getDayTableCellState(props2);
return normalize.element({
...parts.tableCellTrigger.attrs,
id: getCellTriggerId(scope, value.toString()),
role: "button",
dir: prop("dir"),
tabIndex: cellState.focused ? 0 : -1,
"aria-label": cellState.ariaLabel,
"aria-disabled": ariaAttr(!cellState.selectable),
"aria-invalid": ariaAttr(cellState.invalid),
"data-disabled": dataAttr(!cellState.selectable),
"data-selected": dataAttr(cellState.selected),
"data-value": value.toString(),
"data-view": "day",
"data-today": dataAttr(cellState.today),
"data-focus": dataAttr(cellState.focused),
"data-unavailable": dataAttr(cellState.unavailable),
"data-range-start": dataAttr(cellState.firstInRange),
"data-range-end": dataAttr(cellState.lastInRange),
"data-in-range": dataAttr(cellState.inRange),
"data-outside-range": dataAttr(cellState.outsideRange),
"data-weekend": dataAttr(cellState.weekend),
"data-in-hover-range": dataAttr(cellState.inHoveredRange),
"data-hover-range-start": dataAttr(cellState.firstInHoveredRange),
"data-hover-range-end": dataAttr(cellState.lastInHoveredRange),
onClick(event) {
if (event.defaultPrevented) return;
if (!cellState.selectable) return;
send({ type: "CELL.CLICK", cell: "day", value });
},
onPointerMove: isRangePicker ? (event) => {
if (event.pointerType === "touch") return;
if (!cellState.selectable) return;
const focus = event.currentTarget.ownerDocument.activeElement !== event.currentTarget;
if (hoveredValue && isEqualDay(value, hoveredValue)) return;
send({ type: "CELL.POINTER_MOVE", cell: "day", value, focus });
} : void 0
});
},
getMonthTableCellState,
getMonthTableCellProps(props2) {
const { value, columns } = props2;
const cellState = getMonthTableCellState(props2);
return normalize.element({
...parts.tableCell.attrs,
dir: prop("dir"),
colSpan: columns,
role: "gridcell",
"aria-selected": ariaAttr(cellState.selected || cellState.inRange),
"data-selected": dataAttr(cellState.selected),
"aria-disabled": ariaAttr(!cellState.selectable),
"data-value": value
});
},
getMonthTableCellTriggerProps(props2) {
const { value } = props2;
const cellState = getMonthTableCellState(props2);
return normalize.element({
...parts.tableCellTrigger.attrs,
dir: prop("dir"),
role: "button",
id: getCellTriggerId(scope, value.toString()),
"data-selected": dataAttr(cellState.selected),
"aria-disabled": ariaAttr(!cellState.selectable),
"data-disabled": dataAttr(!cellState.selectable),
"data-focus": dataAttr(cellState.focused),
"data-in-range": dataAttr(cellState.inRange),
"aria-label": cellState.valueText,
"data-view": "month",
"data-value": value,
tabIndex: cellState.focused ? 0 : -1,
onClick(event) {
if (event.defaultPrevented) return;
if (!cellState.selectable) return;
send({ type: "CELL.CLICK", cell: "month", value });
},
onPointerMove: isRangePicker ? (event) => {
if (event.pointerType === "touch") return;
if (!cellState.selectable) return;
const focus = event.currentTarget.ownerDocument.activeElement !== event.currentTarget;
if (hoveredValue && cellState.value && isEqualDay(cellState.value, hoveredValue)) return;
send({ type: "CELL.POINTER_MOVE", cell: "month", value: cellState.value, focus });
} : void 0
});
},
getYearTableCellState,
getYearTableCellProps(props2) {
const { value, columns } = props2;
const cellState = getYearTableCellState(props2);
return normalize.element({
...parts.tableCell.attrs,
dir: prop("dir"),
colSpan: columns,
role: "gridcell",
"aria-selected": ariaAttr(cellState.selected),
"data-selected": dataAttr(cellState.selected),
"aria-disabled": ariaAttr(!cellState.selectable),
"data-value": value
});
},
getYearTableCellTriggerProps(props2) {
const { value } = props2;
const cellState = getYearTableCellState(props2);
return normalize.element({
...parts.tableCellTrigger.attrs,
dir: prop("dir"),
role: "button",
id: getCellTriggerId(scope, value.toString()),
"data-selected": dataAttr(cellState.selected),
"data-focus": dataAttr(cellState.focused),
"data-in-range": dataAttr(cellState.inRange),
"aria-disabled": ariaAttr(!cellState.selectable),
"data-disabled": dataAttr(!cellState.selectable),
"aria-label": cellState.valueText,
"data-value": value,
"data-view": "year",
tabIndex: cellState.focused ? 0 : -1,
onClick(event) {
if (event.defaultPrevented) return;
if (!cellState.selectable) return;
send({ type: "CELL.CLICK", cell: "year", value });
}
});
},
getNextTriggerProps(props2 = {}) {
const { view = "day" } = props2;
const isDisabled = disabled || !computed("isNextVisibleRangeValid");
return normalize.button({
...parts.nextTrigger.attrs,
dir: prop("dir"),
id: getNextTriggerId(scope, view),
type: "button",
"aria-label": translations.nextTrigger(view),
disabled: isDisabled,
"data-disabled": dataAttr(isDisabled),
onClick(event) {
if (event.defaultPrevented) return;
send({ type: "GOTO.NEXT", view });
}
});
},
getPrevTriggerProps(props2 = {}) {
const { view = "day" } = props2;
const isDisabled = disabled || !computed("isPrevVisibleRangeValid");
return normalize.button({
...parts.prevTrigger.attrs,
dir: prop("dir"),
id: getPrevTriggerId(scope, view),
type: "button",
"aria-label": translations.prevTrigger(view),
disabled: isDisabled,
"data-disabled": dataAttr(isDisabled),
onClick(event) {
if (event.defaultPrevented) return;
send({ type: "GOTO.PREV", view });
}
});
},
getClearTriggerProps() {
return normalize.button({
...parts.clearTrigger.attrs,
id: getClearTriggerId(scope),
dir: prop("dir"),
type: "button",
"aria-label": translations.clearTrigger,
hidden: !selectedValue.length,
onClick(event) {
if (event.defaultPrevented) return;
send({ type: "VALUE.CLEAR" });
}
});
},
getTriggerProps() {
return normalize.button({
...parts.trigger.attrs,
id: getTriggerId(scope),
dir: prop("dir"),
type: "button",
"data-placement": currentPlacement,
"aria-label": translations.trigger(open),
"aria-controls": getContentId(scope),
"data-state": open ? "open" : "closed",
"aria-haspopup": "grid",
disabled,
onClick(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
send({ type: "TRIGGER.CLICK" });
}
});
},
getViewProps(props2 = {}) {
const { view = "day" } = props2;
return normalize.element({
...parts.view.attrs,
"data-view": view,
hidden: context.get("view") !== view
});
},
getViewTriggerProps(props2 = {}) {
const { view = "day" } = props2;
return normalize.button({
...parts.viewTrigger.attrs,
"data-view": view,
dir: prop("dir"),
id: getViewTriggerId(scope, view),
type: "button",
disabled,
"aria-label": translations.viewTrigger(view),
onClick(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
send({ type: "VIEW.TOGGLE", src: "viewTrigger" });
}
});
},
getViewControlProps(props2 = {}) {
const { view = "day" } = props2;
return normalize.element({
...parts.viewControl.attrs,
"data-view": view,
dir: prop("dir")
});
},
getInputProps(props2 = {}) {
const { index = 0, fixOnBlur = true } = props2;
return normalize.input({
...parts.input.attrs,
id: getInputId(scope, index),
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
dir: prop("dir"),
name: prop("name"),
"data-index": index,
"data-state": open ? "open" : "closed",
readOnly,
disabled,
placeholder: prop("placeholder") || getInputPlaceholder(locale),
defaultValue: computed("valueAsString")[index],
onBeforeInput(event) {
const { data } = getNativeEvent(event);
if (!isValidCharacter(data, separator)) {
event.preventDefault();
}
},
onFocus() {
send({ type: "INPUT.FOCUS", index });
},
onBlur(event) {
const value = event.currentTarget.value.trim();
send({ type: "INPUT.BLUR", value, index, fixOnBlur });
},
onKeyDown(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
const keyMap = {
Enter(event2) {
if (isComposingEvent(event2)) return;
if (isUnavailable(focusedValue)) return;
if (event2.currentTarget.value.trim() === "") return;
send({ type: "INPUT.ENTER", value: event2.currentTarget.value, index });
}
};
const exec = keyMap[event.key];
if (exec) {
exec(event);
event.preventDefault();
}
},
onInput(event) {
const value = event.currentTarget.value;
send({ type: "INPUT.CHANGE", value: ensureValidCharacters(value, separator), index });
}
});
},
getMonthSelectProps() {
return normalize.select({
...parts.monthSelect.attrs,
id: getMonthSelectId(scope),
"aria-label": translations.monthSelect,
disabled,
dir: prop("dir"),
defaultValue: startValue.month,
onChange(event) {
focusMonth(Number(event.currentTarget.value));
}
});
},
getYearSelectProps() {
return normalize.select({
...parts.yearSelect.attrs,
id: getYearSelectId(scope),
disabled,
"aria-label": translations.yearSelect,
dir: prop("dir"),
defaultValue: startValue.year,
onChange(event) {
focusYear(Number(event.currentTarget.value));
}
});
},
getPositionerProps() {
return normalize.element({
id: getPositionerId(scope),
...parts.positioner.attrs,
dir: prop("dir"),
style: popperStyles.floating
});
},
getPresetTriggerProps(props2) {
const value = Array.isArray(props2.value) ? props2.value : getDateRangePreset(props2.value, locale, timeZone);
const valueAsString = value.map((item) => item.toDate(timeZone).toDateString());
return normalize.button({
...parts.presetTrigger.attrs,
"aria-label": translations.presetTrigger(valueAsString),
type: "button",
onClick(event) {
if (event.defaultPrevented) return;
send({ type: "PRESET.CLICK", value });
}
});
}
};
}
var { and } = createGuards();
function isDateArrayEqual(a, b) {
if (a?.length !== b?.length) return false;
const len = Math.max(a.length, b.length);
for (let i = 0; i < len; i++) {
if (!isDateEqual(a[i], b[i])) return false;
}
return true;
}
var machine = createMachine({
props({ props: props2 }) {
const locale = props2.locale || "en-US";
const timeZone = props2.timeZone || "UTC";
const selectionMode = props2.selectionMode || "single";
const numOfMonths = props2.numOfMonths || 1;
const defaultValue = props2.defaultValue ? sortDates(props2.defaultValue).map((date) => constrainValue(date, props2.min, props2.max)) : void 0;
const value = props2.value ? sortDates(props2.value).map((date) => constrainValue(date, props2.min, props2.max)) : void 0;
let focusedValue = props2.focusedValue || props2.defaultFocusedValue || value?.[0] || defaultValue?.[0] || getTodayDate(timeZone);
focusedValue = constrainValue(focusedValue, props2.min, props2.max);
const minView = "day";
const maxView = "year";
const defaultView = clampView(props2.view || minView, minView, maxView);
return {
locale,
numOfMonths,
timeZone,
selectionMode,
defaultView,
minView,
maxView,
outsideDaySelectable: false,
closeOnSelect: true,
format(date, { locale: locale2, timeZone: timeZone2 }) {
const formatter = new DateFormatter(locale2, { timeZone: timeZone2, day: "2-digit", month: "2-digit", year: "numeric" });
return formatter.format(date.toDate(timeZone2));
},
parse(value2, { locale: locale2, timeZone: timeZone2 }) {
return parseDateString(value2, locale2, timeZone2);
},
...props2,
focusedValue: typeof props2.focusedValue === "undefined" ? void 0 : focusedValue,
defaultFocusedValue: focusedValue,
value,
defaultValue: defaultValue ?? [],
positioning: {
placement: "bottom",
...props2.positioning
}
};
},
initialState({ prop }) {
const open = prop("open") || prop("defaultOpen") || prop("inline");
return open ? "open" : "idle";
},
refs() {
return {
announcer: void 0
};
},
context({ prop, bindable, getContext }) {
return {
focusedValue: bindable(() => ({
defaultValue: prop("defaultFocusedValue"),
value: prop("focusedValue"),
isEqual: isDateEqual,
hash: (v) => v.toString(),
sync: true,
onChange(focusedValue) {
const context = getContext();
const view = context.get("view");
const value = context.get("value");
const valueAsString = value.map(
(date) => prop("format")(date, { locale: prop("locale"), timeZone: prop("timeZone") })
);
prop("onFocusChange")?.({ value, valueAsString, view, focusedValue });
}
})),
value: bindable(() => ({
defaultValue: prop("defaultValue"),
value: prop("value"),
isEqual: isDateArrayEqual,
hash: (v) => v.map((date) => date.toString()).join(","),
onChange(value) {
const context = getContext();
const valueAsString = value.map(
(date) => prop("format")(date, { locale: prop("locale"), timeZone: prop("timeZone") })
);
prop("onValueChange")?.({ value, valueAsString, view: context.get("view") });
}
})),
inputValue: bindable(() => ({
defaultValue: ""
})),
activeIndex: bindable(() => ({
defaultValue: 0,
sync: true
})),
hoveredValue: bindable(() => ({
defaultValue: null,
isEqual: (a, b) => b !== null && a !== null && isDateEqual(a, b)
})),
view: bindable(() => ({
defaultValue: prop("defaultView"),
value: prop("view"),
onChange(value) {
prop("onViewChange")?.({ view: value });
}
})),
startValue: bindable(() => {
const focusedValue = prop("focusedValue") || prop("defaultFocusedValue");
return {
defaultValue: alignDate(focusedValue, "start", { months: prop("numOfMonths") }, prop("locale")),
isEqual: isDateEqual
};
}),
currentPlacement: bindable(() => ({
defaultValue: void 0
})),
restoreFocus: bindable(() => ({
defaultValue: false
}))
};
},
computed: {
isInteractive: ({ prop }) => !prop("disabled") && !prop("readOnly"),
visibleDuration: ({ prop }) => ({ months: prop("numOfMonths") }),
endValue: ({ context, computed }) => getEndDate(context.get("startValue"), computed("visibleDuration")),
visibleRange: ({ context, computed }) => ({ start: context.get("startValue"), end: computed("endValue") }),
visibleRangeText({ context, prop, computed }) {
const timeZone = prop("timeZone");
const formatter = new DateFormatter(prop("locale"), { month: "long", year: "numeric", timeZone });
const start = formatter.format(context.get("startValue").toDate(timeZone));
const end = formatter.format(computed("endValue").toDate(timeZone));
const formatted = prop("selectionMode") === "range" ? `${start} - ${end}` : start;
return { start, end, formatted };
},
isPrevVisibleRangeValid: ({ context, prop }) => !isPreviousRangeInvalid(context.get("startValue"), prop("min"), prop("max")),
isNextVisibleRangeValid: ({ prop, computed }) => !isNextRangeInvalid(computed("endValue"), prop("min"), prop("max")),
valueAsString({ context, prop }) {
const value = context.get("value");
return value.map((date) => prop("format")(date, { locale: prop("locale"), timeZone: prop("timeZone") }));
}
},
effects: ["setupLiveRegion"],
watch({ track, prop, context, action, computed }) {
track([() => prop("locale")], () => {
action(["setStartValue"]);
});
track([() => context.hash("focusedValue")], () => {
action([
"setStartValue",
"syncMonthSelectElement",
"syncYearSelectElement",
"focusActiveCellIfNeeded",
"setHoveredValueIfKeyboard"
]);
});
track([() => context.get("inputValue")], () => {
action(["syncInputValue"]);
});
track([() => context.hash("value")], () => {
action(["syncInputElement"]);
});
track([() => computed("valueAsString").toString()], () => {
action(["announceValueText"]);
});
track([() => context.get("view")], () => {
action(["focusActiveCell"]);
});
track([() => prop("open")], () => {
action(["toggleVisibility"]);
});
},
on: {
"VALUE.SET": {
actions: ["setDateValue", "setFocusedDate"]
},
"VIEW.SET": {
actions: ["setView"]
},
"FOCUS.SET": {
actions: ["setFocusedDate"]
},
"VALUE.CLEAR": {
actions: ["clearDateValue", "clearFocusedDate", "focusFirstInputElement"]
},
"INPUT.CHANGE": [
{
guard: "isInputValueEmpty",
actions: ["setInputValue", "clearDateValue", "clearFocusedDate"]
},
{
actions: ["setInputValue", "focusParsedDate"]
}
],
"INPUT.ENTER": {
actions: ["focusParsedDate", "selectFocusedDate"]
},
"INPUT.FOCUS": {
actions: ["setActiveIndex"]
},
"INPUT.BLUR": [
{
guard: "shouldFixOnBlur",
actions: ["setActiveIndexToStart", "selectParsedDate"]
},
{
actions: ["setActiveIndexToStart"]
}
],
"PRESET.CLICK": [
{
guard: "isOpenControlled",
actions: ["setDateValue", "setFocusedDate", "invokeOnClose"]
},
{
target: "focused",
actions: ["setDateValue", "setFocusedDate", "focusInputElement"]
}
],
"GOTO.NEXT": [
{
guard: "isYearView",
actions: ["focusNextDecade", "announceVisibleRange"]
},
{
guard: "isMonthView",
actions: ["focusNextYear", "announceVisibleRange"]
},
{
actions: ["focusNextPage"]
}
],
"GOTO.PREV": [
{
guard: "isYearView",
actions: ["focusPreviousDecade", "announceVisibleRange"]
},
{
guard: "isMonthView",
actions: ["focusPreviousYear", "announceVisibleRange"]
},
{
actions: ["focusPreviousPage"]
}
]
},
states: {
idle: {
tags: ["closed"],
on: {
"CONTROLLED.OPEN": {
target: "open",
actions: ["focusFirstSelectedDate", "focusActiveCell"]
},
"TRIGGER.CLICK": [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"]
}
],
OPEN: [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"]
}
]
}
},
focused: {
tags: ["closed"],
on: {
"CONTROLLED.OPEN": {
target: "open",
actions: ["focusFirstSelectedDate", "focusActiveCell"]
},
"TRIGGER.CLICK": [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"]
}
],
OPEN: [
{
guard: "isOpenControlled",
actions: ["invokeOnOpen"]
},
{
target: "open",
actions: ["focusFirstSelectedDate", "focusActiveCell", "invokeOnOpen"]
}
]
}
},
open: {
tags: ["open"],
effects: ["trackDismissableElement", "trackPositioning"],
exit: ["clearHoveredDate", "resetView"],
on: {
"CONTROLLED.CLOSE": [
{
guard: and("shouldRestoreFocus", "isInteractOutsideEvent"),
target: "focused",
actions: ["focusTriggerElement"]
},
{
guard: "shouldRestoreFocus",
target: "focused",
actions: ["focusInputElement"]
},
{
target: "idle"
}
],
"CELL.CLICK": [
{
guard: "isAboveMinView",
actions: ["setFocusedValueForView", "setPreviousView"]
},
{
guard: and("isRangePicker", "hasSelectedRange"),
actions: ["setActiveIndexToStart", "resetSelection", "setActiveIndexToEnd"]
},
// === Grouped transitions (based on `closeOnSelect` and `isOpenControlled`) ===
{
guard: and("isRangePicker", "isSelectingEndDate", "closeOnSelect", "isOpenControlled"),
actions: ["setFocusedDate", "setSelectedDate", "setActiveIndexToStart", "invokeOnClose", "setRestoreFocus"]
},
{
guard: and("isRangePicker", "isSelectingEndDate", "closeOnSelect"),
target: "focused",
actions: [
"setFocusedDate",
"setSelectedDate",
"setActiveIndexToStart",
"invokeOnClose",
"focusInputElement"
]
},
{
guard: and("isRangePicker", "isSelectingEndDate"),
actions: ["setFocusedDate", "setSelectedDate", "setActiveIndexToStart", "clearHoveredDate"]
},
// ===
{
guard: "isRangePicker",
actions: ["setFocusedDate", "setSelectedDate", "setActiveIndexToEnd"]
},
{
guard: "isMultiPicker",
actions: ["setFocusedDate", "toggleSelectedDate"]
},
// === Grouped transitions (based on `closeOnSelect` and `isOpenControlled`) ===
{
guard: and("closeOnSelect", "isOpenControlled"),
actions: ["setFocusedDate", "setSelectedDate", "invokeOnClose"]
},
{
guard: "closeOnSelect",
target: "focused",
actions: ["setFocusedDate", "setSelectedDate", "invokeOnClose", "focusInputElement"]
},
{
actions: ["setFocusedDate", "setSelectedDate"]
}
// ===
],
"CELL.POINTER_MOVE": {
guard: and("isRangePicker", "isSelectingEndDate"),
actions: ["setHoveredDate", "setFocusedDate"]
},
"TABLE.POINTER_LEAVE": {
guard: "isRangePicker",
actions: ["clearHoveredDate"]
},
"TABLE.POINTER_DOWN": {
actions: ["disableTextSelection"]
},
"TABLE.POINTER_UP": {
actions: ["enableTextSelection"]
},
"TABLE.ESCAPE": [
{
guard: "isOpenControlled",
actions: ["focusFirstSelectedDate", "invokeOnClose"]
},
{
target: "focused",
actions: ["focusFirstSelectedDate", "invokeOnClose", "focusTriggerElement"]
}
],
"TABLE.ENTER": [
{
guard: "isAboveMinView",
actions: ["setPreviousView"]
},
{
guard: and("isRangePicker", "hasSelectedRange"),
actions: ["setActiveIndexToStart", "clearDateValue", "setSelectedDate", "setActiveIndexToEnd"]
},
// === Grouped transitions (based on `closeOnSelect` and `isOpenControlled`) ===
{
guard: and("isRangePicker", "isSelectingEndDate", "closeOnSelect", "isOpenControlled"),
actions: ["setSelectedDate", "setActiveIndexToStart", "invokeOnClose"]
},
{
guard: and("isRangePicker", "isSelectingEndDate", "closeOnSelect"),
target: "focused",
actions: ["setSelectedDate", "setActiveIndexToStart", "invokeOnClose", "focusInputElement"]
},
{
guard: and("isRangePicker", "isSelectingEndDate"),
actions: ["setSelectedDate", "setActiveIndexToStart"]
},
// ===
{
guard: "isRangePicker",
actions: ["setSelectedDate", "setActiveIndexToEnd", "focusNextDay"]
},
{
guard: "isMultiPicker",
actions: ["toggleSelectedDate"]
},
// === Grouped transitions (based on `closeOnSelect` and `isOpenControlled`) ===
{
guard: and("closeOnSelect", "isOpenControlled"),
actions: ["selectFocusedDate", "invokeOnClose"]
},
{
guard: "closeOnSelect",
target: "focused",
actions: ["selectFocusedDate", "invokeOnClose", "focusInputElement"]
},
{
actions: ["selectFocusedDate"]
}
// ===
],
"TABLE.ARROW_RIGHT": [
{
guard: "isMonthView",
actions: ["focusNextMonth"]
},
{
guard: "isYearView",
actions: ["focusNextYear"]
},
{
actions: ["focusNextDay", "setHoveredDate"]
}
],
"TABLE.ARROW_LEFT": [
{
guard: "isMonthView",
actions: ["focusPreviousMonth"]
},
{
guard: "isYearView",
actions: ["focusPreviousYear"]
},
{
actions: ["focusPreviousDay"]
}
],
"TABLE.ARROW_UP": [
{
guard: "isMonthView",
actions: ["focus