UNPKG

@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
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