@zag-js/date-picker
Version:
Core logic for the date-picker widget implemented as a state machine
916 lines (915 loc) • 35.2 kB
JavaScript
// src/date-picker.connect.ts
import {
DateFormatter,
isEqualDay,
isEqualMonth,
isEqualYear,
isSameDay,
isToday,
isWeekend,
toCalendarDateTime
} from "@internationalized/date";
import {
constrainValue,
ensureValidCharacters,
getDateRangePreset,
getDayFormatter,
getDaysInWeek,
getDecadeRange,
getLocaleSeparator,
getMonthDays,
getMonthFormatter,
getMonthNames,
getTodayDate,
getUnitDuration,
getWeekDays,
getWeekOfYear,
getDefaultYearRange,
getYearsRange,
isDateOutsideRange,
isDateUnavailable,
isValidCharacter
} from "@zag-js/date-utils";
import { ariaAttr, dataAttr, getEventKey, getNativeEvent, isComposingEvent } from "@zag-js/dom-query";
import { getPlacementSide, getPlacementStyles } from "@zag-js/popper";
import { chunk, isValueWithinRange } from "@zag-js/utils";
import { parts } from "./date-picker.anatomy.mjs";
import * as dom from "./date-picker.dom.mjs";
import {
adjustStartAndEndDate,
defaultTranslations,
getInputPlaceholder,
getRoleDescription,
isDateWithinRange
} from "./date-picker.utils.mjs";
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 = Boolean(prop("disabled"));
const readOnly = Boolean(prop("readOnly"));
const invalid = Boolean(prop("invalid"));
const interactive = computed("isInteractive");
const empty = selectedValue.length === 0;
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 isMultiPicker = prop("selectionMode") === "multiple";
const isDateUnavailableFn = prop("isDateUnavailable");
const maxSelectedDates = prop("maxSelectedDates");
const isMaxSelected = isMultiPicker && maxSelectedDates != null && selectedValue.length >= maxSelectedDates;
const currentPlacement = context.get("currentPlacement");
const currentPlacementSide = currentPlacement ? getPlacementSide(currentPlacement) : void 0;
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(props = {}) {
const { format } = props;
return getMonthNames(locale, format, focusedValue).map((label, index) => {
const value = index + 1;
const dateValue = focusedValue.set({ month: value });
const disabled2 = isDateOutsideRange(dateValue, min, max);
return { label, value, disabled: disabled2 };
});
}
function getYears() {
const defaultRange = getDefaultYearRange(focusedValue, min, max);
const range = getYearsRange(defaultRange);
return range.map((year) => ({
label: year.toString(),
value: year,
disabled: !isValueWithinRange(year, min?.year, max?.year)
}));
}
function isUnavailable(date) {
return isDateUnavailable(date, isDateUnavailableFn, locale, min, max);
}
function focusMonth(month) {
const date = startValue ?? getTodayDate(timeZone, focusedValue.calendar);
send({ type: "FOCUS.SET", value: date.set({ month }) });
}
function focusYear(year) {
const date = startValue ?? getTodayDate(timeZone, focusedValue.calendar);
send({ type: "FOCUS.SET", value: date.set({ year }) });
}
function getYearTableCellState(props) {
const { value, disabled: disabled2 } = props;
const dateValue = focusedValue.set({ year: value });
const decadeYears = getDecadeRange(startValue.year, { strict: true });
const isOutsideVisibleRange = !decadeYears.includes(value);
const isWithinMinMax = isValueWithinRange(value, min?.year, max?.year);
const isInSelectedRange = isRangePicker && isDateWithinRange(dateValue, selectedValue);
const isFirstInSelectedRange = isRangePicker && selectedValue[0] && isEqualYear(dateValue, selectedValue[0]);
const isLastInSelectedRange = isRangePicker && selectedValue[1] && isEqualYear(dateValue, selectedValue[1]);
const hasHoveredRange = isRangePicker && hoveredRangeValue.length > 0;
const isInHoveredRange = hasHoveredRange && isDateWithinRange(dateValue, hoveredRangeValue);
const isFirstInHoveredRange = hasHoveredRange && hoveredRangeValue[0] && isEqualYear(dateValue, hoveredRangeValue[0]);
const isLastInHoveredRange = hasHoveredRange && hoveredRangeValue[1] && isEqualYear(dateValue, hoveredRangeValue[1]);
const cellState = {
focused: focusedValue.year === props.value,
selectable: !isOutsideVisibleRange && isWithinMinMax,
outsideRange: isOutsideVisibleRange,
selected: !!selectedValue.find((date) => date && date.year === value),
valueText: value.toString(),
inRange: isInSelectedRange || isInHoveredRange,
firstInRange: !!isFirstInSelectedRange,
lastInRange: !!isLastInSelectedRange,
inHoveredRange: !!isInHoveredRange,
firstInHoveredRange: !!isFirstInHoveredRange,
lastInHoveredRange: !!isLastInHoveredRange,
value: dateValue,
get disabled() {
return disabled2 || !cellState.selectable;
}
};
return cellState;
}
function getMonthTableCellState(props) {
const { value, disabled: disabled2 } = props;
const dateValue = focusedValue.set({ month: value });
const formatter = getMonthFormatter(locale, timeZone, focusedValue);
const isInSelectedRange = isRangePicker && isDateWithinRange(dateValue, selectedValue);
const isFirstInSelectedRange = isRangePicker && selectedValue[0] && isEqualMonth(dateValue, selectedValue[0]);
const isLastInSelectedRange = isRangePicker && selectedValue[1] && isEqualMonth(dateValue, selectedValue[1]);
const hasHoveredRange = isRangePicker && hoveredRangeValue.length > 0;
const isInHoveredRange = hasHoveredRange && isDateWithinRange(dateValue, hoveredRangeValue);
const isFirstInHoveredRange = hasHoveredRange && hoveredRangeValue[0] && isEqualMonth(dateValue, hoveredRangeValue[0]);
const isLastInHoveredRange = hasHoveredRange && hoveredRangeValue[1] && isEqualMonth(dateValue, hoveredRangeValue[1]);
const cellState = {
focused: focusedValue.month === props.value,
selectable: !isDateOutsideRange(dateValue, min, max),
selected: !!selectedValue.find((date) => date && date.month === value && date.year === focusedValue.year),
valueText: formatter.format(dateValue.toDate(timeZone)),
inRange: isInSelectedRange || isInHoveredRange,
firstInRange: !!isFirstInSelectedRange,
lastInRange: !!isLastInSelectedRange,
inHoveredRange: !!isInHoveredRange,
firstInHoveredRange: !!isFirstInHoveredRange,
lastInHoveredRange: !!isLastInHoveredRange,
outsideRange: false,
value: dateValue,
get disabled() {
return disabled2 || !cellState.selectable;
}
};
return cellState;
}
function getDayTableCellState(props) {
const { value, disabled: disabled2, visibleRange = computed("visibleRange") } = props;
const formatter = getDayFormatter(locale, timeZone, focusedValue);
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 && selectedValue[0] && isSameDay(value, selectedValue[0]);
const isLastInSelectedRange = isRangePicker && selectedValue[1] && isSameDay(value, selectedValue[1]);
const hasHoveredRange = isRangePicker && hoveredRangeValue.length > 0;
const isInHoveredRange = hasHoveredRange && isDateWithinRange(value, hoveredRangeValue);
const isFirstInHoveredRange = hasHoveredRange && hoveredRangeValue[0] && isSameDay(value, hoveredRangeValue[0]);
const isLastInHoveredRange = hasHoveredRange && hoveredRangeValue[1] && isSameDay(value, hoveredRangeValue[1]);
const isSelected = selectedValue.some((date) => date != null && isSameDay(value, date));
const cellState = {
invalid: isDateOutsideRange(value, min, max),
disabled: disabled2 || !outsideDaySelectable && isOutsideRange || isDateOutsideRange(value, min, max) || // Disable unselected dates when max is reached in multiple selection mode
isMaxSelected && !isSelected,
selected: isSelected,
unavailable: isDateUnavailable(value, isDateUnavailableFn, locale, min, max) && !disabled2,
outsideRange: isOutsideRange,
today: isToday(value, timeZone),
weekend: isWeekend(value, locale),
value,
valueText: formatter.format(value.toDate(timeZone)),
get focused() {
return focusedValue != null && isSameDay(value, focusedValue) && (!cellState.outsideRange || outsideDaySelectable);
},
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(props) {
const { view = "day", id } = props;
return [view, id].filter(Boolean).join(" ");
}
return {
focused,
open,
disabled,
invalid,
readOnly,
inline: !!prop("inline"),
numOfMonths: prop("numOfMonths"),
showWeekNumbers: !!prop("showWeekNumbers"),
selectionMode: prop("selectionMode"),
maxSelectedDates,
isMaxSelected,
view: context.get("view"),
getRangePresetValue(preset) {
return getDateRangePreset(preset, locale, timeZone);
},
getWeekNumber(week) {
const firstDay = week[0];
return firstDay ? getWeekOfYear(firstDay, locale) : 0;
},
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, focusedValue);
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(startValue, startOfWeek, timeZone, locale),
visibleRangeText: computed("visibleRangeText"),
value: selectedValue,
valueAsDate: selectedValue.filter((date) => date != null).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, focusedValue.calendar), min, max);
send({ type: "VALUE.SET", value: [value] });
},
setValue(values) {
const computedValue = values.map((date) => constrainValue(date, min, max));
send({ type: "VALUE.SET", value: computedValue });
},
setTime(time, index = 0) {
const values = Array.from(selectedValue);
let dateValue = values[index];
if (!dateValue) return;
if (!("hour" in dateValue)) {
dateValue = toCalendarDateTime(dateValue);
}
dateValue = dateValue.set({
hour: time.hour ?? ("hour" in dateValue ? dateValue.hour : 0),
minute: time.minute ?? ("minute" in dateValue ? dateValue.minute : 0),
second: time.second ?? ("second" in dateValue ? dateValue.second : 0),
millisecond: time.millisecond ?? ("millisecond" in dateValue ? dateValue.millisecond : 0)
});
values[index] = constrainValue(dateValue, min, max);
send({ type: "VALUE.SET", value: values });
},
clearValue(options = {}) {
const { focus = true } = options;
send({ type: "VALUE.CLEAR", focus });
},
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(props = {}) {
const { columns = 1 } = props;
const years = getDecadeRange(startValue.year, { strict: true }).map((year) => ({
label: year.toString(),
value: year,
disabled: !isValueWithinRange(year, min?.year, max?.year)
}));
return chunk(years, columns);
},
getDecade() {
const years = getDecadeRange(startValue.year, { strict: true });
return { start: years.at(0), end: years.at(-1) };
},
getMonthsGrid(props = {}) {
const { columns = 1, format } = props;
return chunk(getMonths({ format }), columns);
},
format(value, opts = { month: "long", year: "numeric" }) {
return new DateFormatter(locale, {
...opts,
calendar: value.calendar.identifier
}).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: dom.getRootId(scope),
"data-state": open ? "open" : "closed",
"data-disabled": dataAttr(disabled),
"data-readonly": dataAttr(readOnly),
"data-empty": dataAttr(empty)
});
},
getLabelProps(props = {}) {
const { index = 0 } = props;
return normalize.label({
...parts.label.attrs,
id: dom.getLabelId(scope, index),
dir: prop("dir"),
htmlFor: dom.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: dom.getControlId(scope),
"data-disabled": dataAttr(disabled),
"data-placeholder-shown": dataAttr(empty)
});
},
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,
"data-side": currentPlacementSide,
"data-inline": dataAttr(prop("inline")),
id: dom.getContentId(scope),
tabIndex: -1,
role: "application",
"aria-roledescription": "datepicker",
"aria-label": translations.content
});
},
getTableProps(props = {}) {
const { view = "day", columns = view === "day" ? 7 : 4 } = props;
const uid = getTableId2(props);
return normalize.element({
...parts.table.attrs,
role: "grid",
"data-columns": columns,
"aria-roledescription": getRoleDescription(view),
id: dom.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(props = {}) {
const { view = "day" } = props;
return normalize.element({
...parts.tableHead.attrs,
"aria-hidden": true,
dir: prop("dir"),
"data-view": view,
"data-disabled": dataAttr(disabled)
});
},
getTableHeaderProps(props = {}) {
const { view = "day" } = props;
return normalize.element({
...parts.tableHeader.attrs,
dir: prop("dir"),
"data-view": view,
"data-disabled": dataAttr(disabled)
});
},
getTableBodyProps(props = {}) {
const { view = "day" } = props;
return normalize.element({
...parts.tableBody.attrs,
"data-view": view,
"data-disabled": dataAttr(disabled)
});
},
getTableRowProps(props = {}) {
const { view = "day" } = props;
return normalize.element({
...parts.tableRow.attrs,
"aria-disabled": ariaAttr(disabled),
"data-disabled": dataAttr(disabled),
"data-view": view
});
},
getWeekNumberHeaderCellProps(props = {}) {
const { view = "day" } = props;
return normalize.element({
...parts.tableCell.attrs,
scope: "col",
"aria-label": translations.weekColumnHeader,
"data-view": view,
"data-type": "week-number",
"data-disabled": dataAttr(disabled)
});
},
getWeekNumberCellProps(props) {
const { weekIndex, week } = props;
const weekNumber = week[0] ? getWeekOfYear(week[0], locale) : 0;
return normalize.element({
...parts.tableCell.attrs,
role: "rowheader",
"aria-label": translations.weekNumberCell?.(weekNumber),
"data-view": "day",
"data-week-index": weekIndex,
"data-type": "week-number",
"data-disabled": dataAttr(disabled)
});
},
getDayTableCellState,
getDayTableCellProps(props) {
const { value } = props;
const cellState = getDayTableCellState(props);
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(props) {
const { value } = props;
const cellState = getDayTableCellState(props);
return normalize.element({
...parts.tableCellTrigger.attrs,
id: dom.getCellTriggerId(scope, value.toString()),
role: "button",
dir: prop("dir"),
tabIndex: cellState.focused ? 0 : -1,
"aria-label": translations.dayCell(cellState),
"aria-disabled": ariaAttr(!cellState.selectable),
"aria-invalid": ariaAttr(cellState.invalid),
"data-disabled": dataAttr(!cellState.selectable),
"data-selectable": 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 = !scope.isActiveElement(event.currentTarget);
if (hoveredValue && isEqualDay(value, hoveredValue)) return;
send({
type: "CELL.POINTER_MOVE",
cell: "day",
value,
focus,
outsideRange: cellState.outsideRange
});
} : void 0
});
},
getMonthTableCellState,
getMonthTableCellProps(props) {
const { value, columns } = props;
const cellState = getMonthTableCellState(props);
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(props) {
const { value } = props;
const cellState = getMonthTableCellState(props);
return normalize.element({
...parts.tableCellTrigger.attrs,
id: dom.getCellTriggerId(scope, value.toString()),
role: "button",
dir: prop("dir"),
tabIndex: cellState.focused ? 0 : -1,
"aria-label": cellState.valueText,
"aria-disabled": ariaAttr(!cellState.selectable),
"data-disabled": dataAttr(!cellState.selectable),
"data-selectable": dataAttr(cellState.selectable),
"data-selected": dataAttr(cellState.selected),
"data-value": value,
"data-view": "month",
"data-focus": dataAttr(cellState.focused),
"data-outside-range": dataAttr(cellState.outsideRange),
"data-range-start": dataAttr(cellState.firstInRange),
"data-range-end": dataAttr(cellState.lastInRange),
"data-in-range": dataAttr(cellState.inRange),
"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: "month", value });
},
onPointerMove: isRangePicker ? (event) => {
if (event.pointerType === "touch") return;
if (!cellState.selectable) return;
const focus = !scope.isActiveElement(event.currentTarget);
if (hoveredValue && cellState.value && isEqualMonth(cellState.value, hoveredValue)) return;
send({ type: "CELL.POINTER_MOVE", cell: "month", value: cellState.value, focus });
} : void 0
});
},
getYearTableCellState,
getYearTableCellProps(props) {
const { value, columns } = props;
const cellState = getYearTableCellState(props);
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
});
},
getYearTableCellTriggerProps(props) {
const { value } = props;
const cellState = getYearTableCellState(props);
return normalize.element({
...parts.tableCellTrigger.attrs,
id: dom.getCellTriggerId(scope, value.toString()),
role: "button",
dir: prop("dir"),
tabIndex: cellState.focused ? 0 : -1,
"aria-label": cellState.valueText,
"aria-disabled": ariaAttr(!cellState.selectable),
"data-disabled": dataAttr(!cellState.selectable),
"data-selectable": dataAttr(cellState.selectable),
"data-selected": dataAttr(cellState.selected),
"data-value": value,
"data-view": "year",
"data-focus": dataAttr(cellState.focused),
"data-outside-range": dataAttr(cellState.outsideRange),
"data-range-start": dataAttr(cellState.firstInRange),
"data-range-end": dataAttr(cellState.lastInRange),
"data-in-range": dataAttr(cellState.inRange),
"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: "year", value });
},
onPointerMove: isRangePicker ? (event) => {
if (event.pointerType === "touch") return;
if (!cellState.selectable) return;
const focus = !scope.isActiveElement(event.currentTarget);
if (hoveredValue && cellState.value && isEqualYear(cellState.value, hoveredValue)) return;
send({ type: "CELL.POINTER_MOVE", cell: "year", value: cellState.value, focus });
} : void 0
});
},
getNextTriggerProps(props = {}) {
const { view = "day" } = props;
const isDisabled = disabled || !computed("isNextVisibleRangeValid");
return normalize.button({
...parts.nextTrigger.attrs,
dir: prop("dir"),
id: dom.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(props = {}) {
const { view = "day" } = props;
const isDisabled = disabled || !computed("isPrevVisibleRangeValid");
return normalize.button({
...parts.prevTrigger.attrs,
dir: prop("dir"),
id: dom.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: dom.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: dom.getTriggerId(scope),
dir: prop("dir"),
type: "button",
"data-placement": currentPlacement,
"data-side": currentPlacementSide,
"aria-label": translations.trigger(open),
"aria-controls": dom.getContentId(scope),
"aria-expanded": open,
"data-state": open ? "open" : "closed",
"data-placeholder-shown": dataAttr(empty),
"aria-haspopup": "grid",
disabled,
onClick(event) {
if (event.defaultPrevented) return;
if (!interactive) return;
send({ type: "TRIGGER.CLICK" });
}
});
},
getViewProps(props = {}) {
const { view = "day" } = props;
return normalize.element({
...parts.view.attrs,
"data-view": view,
hidden: context.get("view") !== view
});
},
getViewTriggerProps(props = {}) {
const { view = "day" } = props;
return normalize.button({
...parts.viewTrigger.attrs,
"data-view": view,
dir: prop("dir"),
id: dom.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(props = {}) {
const { view = "day" } = props;
return normalize.element({
...parts.viewControl.attrs,
"data-view": view,
dir: prop("dir")
});
},
getInputProps(props = {}) {
const { index = 0, fixOnBlur = true } = props;
return normalize.input({
...parts.input.attrs,
id: dom.getInputId(scope, index),
autoComplete: "off",
autoCorrect: "off",
spellCheck: "false",
dir: prop("dir"),
name: prop("name"),
"data-index": index,
"data-state": open ? "open" : "closed",
"data-placeholder-shown": dataAttr(empty),
readOnly,
disabled,
required: prop("required"),
"aria-invalid": ariaAttr(invalid),
"data-invalid": dataAttr(invalid),
placeholder: prop("placeholder") || getInputPlaceholder(locale),
defaultValue: computed("valueAsString")[index],
onBeforeInput(event) {
const { data } = getNativeEvent(event);
if (!isValidCharacter(data, separator)) {
event.preventDefault();
}
},
onClick(event) {
if (event.defaultPrevented) return;
if (!prop("openOnClick")) return;
if (!interactive) return;
send({ type: "OPEN", src: "input.click" });
},
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: dom.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: dom.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: dom.getPositionerId(scope),
...parts.positioner.attrs,
dir: prop("dir"),
style: popperStyles.floating
});
},
getPresetTriggerProps(props) {
const value = Array.isArray(props.value) ? props.value : getDateRangePreset(props.value, locale, timeZone);
const valueAsString = value.filter((item) => item != null).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 });
}
});
}
};
}
export {
connect
};