UNPKG

@esri/calcite-components

Version:

Web Components for Esri's Calcite Design System.

355 lines (354 loc) • 18.9 kB
/*! All material copyright ESRI, All Rights Reserved, unless otherwise specified. See https://github.com/Esri/calcite-design-system/blob/dev/LICENSE.md for details. v3.2.1 */ import { c as customElement } from "../../chunks/runtime.js"; import { ref } from "lit-html/directives/ref.js"; import { keyed } from "lit-html/directives/keyed.js"; import { html } from "lit"; import { LitElement, createEvent, safeClassMap } from "@arcgis/lumina"; import { b as dateFromRange, i as inRange, s as sameDate, k as hasSameMonthAndYear, m as getFirstValidDateInMonth, n as nextMonth } from "../../chunks/date.js"; import { css } from "@lit/reactive-element/css-tag.js"; const CSS = { calendar: "calendar", calendarContainer: "calendar-container", calendarStart: "calendar--start", currentDay: "current-day", dayContainer: "day-container", insideRangeHover: "inside-range--hover", month: "month", noncurrent: "noncurrent", outsideRangeHover: "outside-range--hover", weekDays: "week-days", weekHeader: "week-header", weekHeaderContainer: "week-header-container" }; const styles = css`:host([hidden]){display:none}[hidden]{display:none}.calendar-container{display:flex;inline-size:100%}:host([range][layout=vertical]) .calendar-container{flex-direction:column}.calendar{inline-size:100%}.week-header-container{display:flex;block-size:16px;padding-inline:var(--calcite-spacing-sm);padding-block:var(--calcite-spacing-md)}.week-header{display:flex;align-items:center;justify-content:center;text-align:center;font-size:var(--calcite-font-size--2);line-height:1rem;font-weight:var(--calcite-font-weight-bold);inline-size:14.2857142857%;color:var(--calcite-date-picker-week-header-text-color, var(--calcite-color-text-3))}.day-container{display:flex;inline-size:100%;min-inline-size:0px;justify-content:center}.day-container calcite-date-picker-day{inline-size:100%}.week-days{display:grid;grid-template-columns:repeat(7,1fr);grid-auto-rows:1fr;padding-inline:var(--calcite-spacing-sm);padding-block-end:var(--calcite-spacing-sm)}.month-header{display:flex;inline-size:100%;justify-content:space-between}.month{display:flex;inline-size:100%;flex-direction:column;justify-content:space-between}.day{font-size:var(--calcite-font-size)}:host([scale=s]) .week-days{padding-inline:var(--calcite-spacing-xs);padding-block-end:var(--calcite-spacing-xs)}:host([scale=s]) .week-header-container{padding-inline:var(--calcite-spacing-xs);padding-block:var(--calcite-spacing-sm)}:host([scale=s]) .day{font-size:var(--calcite-font-size-sm)}:host([scale=l]) .week-header{font-size:var(--calcite-font-size--1);line-height:1rem}:host([scale=l]) .week-days{padding-inline:var(--calcite-spacing-md);padding-block-end:var(--calcite-spacing-md)}:host([scale=l]) .week-header-container{padding-inline:var(--calcite-spacing-md);padding-block:var(--calcite-spacing-md-plus)}:host([scale=l]) .day{font-size:var(--calcite-font-size-md)}.calendar--start{border-width:0px;border-style:solid;border-color:var(--calcite-date-picker-range-calendar-divider-color, var(--calcite-color-border-1))}:host([range][layout=horizontal]) .calendar--start{border-inline-end-width:var(--calcite-border-width-sm)}:host([range][layout=vertical]) .calendar--start{border-block-end-width:var(--calcite-border-width-sm)}.noncurrent{pointer-events:none;opacity:0}`; const DAYS_PER_WEEK = 7; const DAYS_MAXIMUM_INDEX = 6; const NUM_DAYS_TO_DISPLAY = 42; class DatePickerMonth extends LitElement { constructor() { super(); this.activeDate = /* @__PURE__ */ new Date(); this.range = false; this.calciteInternalDatePickerDayHover = createEvent({ cancelable: false }); this.calciteInternalDatePickerDaySelect = createEvent({ cancelable: false }); this.calciteInternalDatePickerMonthActiveDateChange = createEvent({ cancelable: false }); this.calciteInternalDatePickerMonthChange = createEvent({ cancelable: false }); this.calciteInternalDatePickerMonthMouseOut = createEvent({ cancelable: false }); this.listen("pointerout", this.pointerOutHandler); this.listen("focusout", this.disableActiveFocus); } static { this.properties = { focusedDate: [16, {}, { state: true }], activeDate: [0, {}, { attribute: false }], dateTimeFormat: [0, {}, { attribute: false }], endDate: [0, {}, { attribute: false }], headingLevel: [11, {}, { type: Number, reflect: true }], hoverRange: [0, {}, { attribute: false }], layout: [3, {}, { reflect: true }], localeData: [0, {}, { attribute: false }], max: [0, {}, { attribute: false }], messages: [0, {}, { attribute: false }], min: [0, {}, { attribute: false }], monthStyle: 1, range: [7, {}, { reflect: true, type: Boolean }], scale: [3, {}, { reflect: true }], selectedDate: [0, {}, { attribute: false }], startDate: [0, {}, { attribute: false }] }; } static { this.styles = styles; } load() { this.focusedDate = this.selectedDate || this.activeDate; } willUpdate(changes) { if (changes.has("activeDate")) { this.updateFocusedDateWithActive(this.activeDate); } if (changes.has("selectedDate")) { this.focusedDate = this.selectedDate; } } updateFocusedDateWithActive(newActiveDate) { if (!this.selectedDate) { this.focusedDate = inRange(newActiveDate, this.min, this.max) ? newActiveDate : dateFromRange(newActiveDate, this.min, this.max); } } keyDownHandler(event) { if (event.defaultPrevented) { return; } const isRTL = this.el.dir === "rtl"; const dateValue = event.target.value; switch (event.key) { case "ArrowUp": event.preventDefault(); this.addDays(-7, dateValue); break; case "ArrowRight": event.preventDefault(); this.addDays(isRTL ? -1 : 1, dateValue); break; case "ArrowDown": event.preventDefault(); this.addDays(7, dateValue); break; case "ArrowLeft": event.preventDefault(); this.addDays(isRTL ? 1 : -1, dateValue); break; case "PageUp": event.preventDefault(); this.addMonths(-1, dateValue); break; case "PageDown": event.preventDefault(); this.addMonths(1, dateValue); break; case "Home": event.preventDefault(); this.activeDate.setDate(1); this.addDays(0, dateValue); break; case "End": event.preventDefault(); this.activeDate.setDate(new Date(this.activeDate.getFullYear(), this.activeDate.getMonth() + 1, 0).getDate()); this.addDays(0, dateValue); break; case "Enter": case " ": event.preventDefault(); break; case "Tab": this.activeFocus = false; } } disableActiveFocus() { this.activeFocus = false; } pointerOutHandler() { this.calciteInternalDatePickerMonthMouseOut.emit(); } addMonths(step, targetDate) { const nextDate = new Date(targetDate); nextDate.setMonth(targetDate.getMonth() + step); this.calciteInternalDatePickerMonthActiveDateChange.emit(dateFromRange(nextDate, this.min, this.max)); this.focusedDate = dateFromRange(nextDate, this.min, this.max); this.activeFocus = true; this.calciteInternalDatePickerDayHover.emit(nextDate); } addDays(step = 0, targetDate) { const nextDate = new Date(targetDate); nextDate.setDate(targetDate.getDate() + step); this.calciteInternalDatePickerMonthActiveDateChange.emit(dateFromRange(nextDate, this.min, this.max)); this.focusedDate = dateFromRange(nextDate, this.min, this.max); this.activeFocus = true; this.calciteInternalDatePickerDayHover.emit(nextDate); } getPreviousMonthDays(month, year, startOfWeek) { const lastDate = new Date(year, month, 0); const date = lastDate.getDate(); const startDay = lastDate.getDay(); const days = []; if (startDay === (startOfWeek + DAYS_MAXIMUM_INDEX) % DAYS_PER_WEEK) { return days; } if (startDay === startOfWeek) { return [date]; } for (let i = (DAYS_PER_WEEK + startDay - startOfWeek) % DAYS_PER_WEEK; i >= 0; i--) { days.push(date - i); } return days; } getCurrentMonthDays(month, year) { const num = new Date(year, month + 1, 0).getDate(); const days = []; for (let i = 0; i < num; i++) { days.push(i + 1); } return days; } getNextMonthDays(month, year, startOfWeek) { const endDay = new Date(year, month + 1, 0).getDay(); const days = []; if (endDay === (startOfWeek + DAYS_MAXIMUM_INDEX) % DAYS_PER_WEEK) { return days; } for (let i = 0; i < (DAYS_MAXIMUM_INDEX - (endDay - startOfWeek)) % DAYS_PER_WEEK; i++) { days.push(i + 1); } return days; } betweenSelectedRange(date) { return !!(this.startDate && this.endDate && date > this.startDate && date < this.endDate && !this.isRangeHover(date)); } isSelected(date) { return !!(sameDate(date, this.selectedDate) || this.startDate && sameDate(date, this.startDate) || this.endDate && sameDate(date, this.endDate)); } isStartOfRange(date) { return !!(this.startDate && !sameDate(this.startDate, this.endDate) && sameDate(this.startDate, date) && !this.isEndOfRange(date)); } isEndOfRange(date) { return !!(this.endDate && !sameDate(this.startDate, this.endDate) && sameDate(this.endDate, date) || !this.endDate && this.hoverRange && sameDate(this.startDate, this.hoverRange.end) && sameDate(date, this.hoverRange.end)); } dayHover(event) { const target = event.target; if (target.disabled) { this.calciteInternalDatePickerMonthMouseOut.emit(); } else { this.calciteInternalDatePickerDayHover.emit(target.value); } event.stopPropagation(); } daySelect(event) { const target = event.target; this.activeFocus = false; this.calciteInternalDatePickerDaySelect.emit(target.value); event.stopPropagation(); } isFocusedOnStart() { return this.hoverRange?.focused === "start"; } isHoverInRange() { if (!this.hoverRange || !this.startDate) { return false; } const { start, end } = this.hoverRange; const isStartFocused = this.isFocusedOnStart(); const isEndAfterStart = this.startDate && end > this.startDate; const isEndBeforeEnd = this.endDate && end < this.endDate; const isStartAfterStart = this.startDate && start > this.startDate; const isStartBeforeEnd = this.endDate && start < this.endDate; const isEndDateAfterStartAndBeforeEnd = !isStartFocused && this.startDate && isEndAfterStart && (!this.endDate || isEndBeforeEnd); const isStartDateBeforeEndAndAfterStart = isStartFocused && this.startDate && isStartAfterStart && isStartBeforeEnd; return isEndDateAfterStartAndBeforeEnd || isStartDateBeforeEndAndAfterStart; } isRangeHover(date) { if (!this.hoverRange) { return false; } const { start, end } = this.hoverRange; const isStartFocused = this.isFocusedOnStart(); const insideRange = this.isHoverInRange(); const isDateBeforeStartDateAndAfterStart = date > start && date < this.startDate; const isDateAfterEndDateAndBeforeEnd = date < end && date > this.endDate; const isDateBeforeEndDateAndAfterEnd = date > end && date < this.endDate; const isDateAfterStartDateAndBeforeStart = date < start && date > this.startDate; const isDateAfterStartDateAndBeforeEnd = date < end && date > this.startDate; const isDateBeforeEndDateAndAfterStart = date > start && date < this.endDate; const hasBothStartAndEndDate = this.startDate && this.endDate; if (insideRange) { if (hasBothStartAndEndDate) { return isStartFocused ? date < this.endDate && (isDateAfterStartDateAndBeforeStart || isDateBeforeStartDateAndAfterStart) : isDateBeforeEndDateAndAfterEnd || isDateAfterEndDateAndBeforeEnd; } else if (this.startDate && !this.endDate) { return isStartFocused ? isDateBeforeStartDateAndAfterStart : isDateAfterStartDateAndBeforeEnd; } else if (!this.startDate && this.endDate) { return isStartFocused ? isDateBeforeEndDateAndAfterStart : isDateAfterEndDateAndBeforeEnd; } } else { if (hasBothStartAndEndDate) { return isStartFocused ? isDateBeforeStartDateAndAfterStart : isDateAfterEndDateAndBeforeEnd; } } } getDays(prevMonthDays, currMonthDays, nextMonthDays, position = "start") { let month = this.activeDate.getMonth(); const nextMonth2 = month + 1; month = position === "end" ? nextMonth2 : month; let dayInWeek = 0; const getDayInWeek = () => dayInWeek++ % 7; const year = this.activeDate.getFullYear(); const days = [ ...prevMonthDays.map((day) => { return { active: false, day, dayInWeek: getDayInWeek(), date: new Date(year, month - 1, day) }; }), ...currMonthDays.map((day) => { const date = new Date(year, month, day); const isCurrentDay = sameDate(date, /* @__PURE__ */ new Date()); const active = this.focusedDate && this.focusedDate !== this.startDate && this.focusedDate !== this.endDate ? sameDate(date, this.focusedDate) : sameDate(date, this.startDate) || sameDate(date, this.endDate); return { active, currentMonth: true, currentDay: isCurrentDay, day, dayInWeek: getDayInWeek(), date, ref: true }; }), ...nextMonthDays.map((day) => { return { active: false, day, dayInWeek: getDayInWeek(), date: new Date(year, nextMonth2, day) }; }) ]; return days; } monthHeaderSelectChange(event) { const date = new Date(event.detail); const target = event.target; this.updateFocusableDate(date); event.stopPropagation(); this.calciteInternalDatePickerMonthChange.emit({ date, position: target.position }); } updateFocusableDate(date) { if (!this.selectedDate || !this.range) { this.focusedDate = this.getFirstValidDateOfMonth(date); } else if (this.selectedDate && this.range) { if (!hasSameMonthAndYear(this.startDate, date) || !hasSameMonthAndYear(this.endDate, date)) { this.focusedDate = this.getFirstValidDateOfMonth(date); } } } getFirstValidDateOfMonth(date) { return date.getDate() === 1 ? date : getFirstValidDateInMonth(date, this.min, this.max); } render() { const month = this.activeDate.getMonth(); const year = this.activeDate.getFullYear(); const startOfWeek = this.localeData.weekStart % 7; const { abbreviated, short, narrow } = this.localeData.days; const weekDays = this.scale === "s" ? narrow || short || abbreviated : short || abbreviated || narrow; const adjustedWeekDays = [...weekDays.slice(startOfWeek, 7), ...weekDays.slice(0, startOfWeek)]; const curMonDays = this.getCurrentMonthDays(month, year); const prevMonDays = this.getPreviousMonthDays(month, year, startOfWeek); const nextMonDays = this.getNextMonthDays(month, year, startOfWeek); const numDaysDisplayed = curMonDays.length + prevMonDays.length + nextMonDays.length; if (numDaysDisplayed < NUM_DAYS_TO_DISPLAY) { const initialDay = nextMonDays.length ? nextMonDays[nextMonDays.length - 1] : 0; for (let i = 1; i <= NUM_DAYS_TO_DISPLAY - numDaysDisplayed; i++) { nextMonDays.push(initialDay + i); } } const nextMonth2 = month + 1; const endCalendarPrevMonDays = this.getPreviousMonthDays(nextMonth2, year, startOfWeek); const endCalendarCurrMonDays = this.getCurrentMonthDays(nextMonth2, year); const endCalendarNextMonDays = this.getNextMonthDays(nextMonth2, year, startOfWeek); const days = this.getDays(prevMonDays, curMonDays, nextMonDays); const nextMonthDays = this.getDays(endCalendarPrevMonDays, endCalendarCurrMonDays, endCalendarNextMonDays, "end"); return html`<div class=${safeClassMap({ [CSS.calendarContainer]: true })} role=grid>${this.renderCalendar(adjustedWeekDays, days)}${this.range && this.renderCalendar(adjustedWeekDays, nextMonthDays, true) || ""}</div>`; } renderDateDay({ active, currentMonth, currentDay, date, day, dayInWeek, ref: ref$1 }, key) { const isDateInRange = inRange(date, this.min, this.max); return keyed(key, html`<div class=${safeClassMap({ [CSS.dayContainer]: true })} role=gridcell><calcite-date-picker-day .active=${active} class=${safeClassMap({ [CSS.currentDay]: currentDay, [CSS.insideRangeHover]: this.isHoverInRange(), [CSS.outsideRangeHover]: !this.isHoverInRange(), [CSS.noncurrent]: this.range && !currentMonth })} .currentMonth=${currentMonth} .dateTimeFormat=${this.dateTimeFormat} .day=${day} .disabled=${!isDateInRange} .endOfRange=${this.isEndOfRange(date)} .highlighted=${this.betweenSelectedRange(date)} @calciteInternalDayHover=${this.dayHover} @calciteInternalDaySelect=${this.daySelect} .range=${!!this.startDate && !!this.endDate && !sameDate(this.startDate, this.endDate)} .rangeEdge=${dayInWeek === 0 ? "start" : dayInWeek === 6 ? "end" : void 0} .rangeHover=${isDateInRange && this.isRangeHover(date)} .scale=${this.scale} .selected=${this.isSelected(date)} .startOfRange=${this.isStartOfRange(date)} .value=${date} ${ref((el) => { if (ref$1 && active && this.activeFocus) { el?.setFocus(); } })}></calcite-date-picker-day></div>`); } renderCalendar(weekDays, days, isEndCalendar = false) { return html`<div class=${safeClassMap({ [CSS.calendar]: true, [CSS.calendarStart]: !isEndCalendar })}><calcite-date-picker-month-header .activeDate=${isEndCalendar ? nextMonth(this.activeDate) : this.activeDate} data-test-calendar=${isEndCalendar ? "end" : "start"} .headingLevel=${this.headingLevel} .localeData=${this.localeData} .max=${this.max} .messages=${this.messages} .min=${this.min} .monthStyle=${this.monthStyle} @calciteInternalDatePickerMonthHeaderSelectChange=${this.monthHeaderSelectChange} .position=${isEndCalendar ? "end" : this.range ? "start" : null} .scale=${this.scale} .selectedDate=${this.selectedDate}></calcite-date-picker-month-header>${this.renderMonthCalendar(weekDays, days, isEndCalendar)}</div>`; } renderMonthCalendar(weekDays, days, isEndCalendar = false) { const endCalendarStartIndex = 50; return html`<div class=${safeClassMap({ [CSS.month]: true })} @keydown=${this.keyDownHandler}><div class=${safeClassMap({ [CSS.weekHeaderContainer]: true })} role=row>${weekDays.map((weekday) => html`<span class=${safeClassMap({ [CSS.weekHeader]: true })} role=columnheader>${weekday}</span>`)}</div><div class=${safeClassMap({ [CSS.weekDays]: true })} role=row>${days.map((day, index) => this.renderDateDay(day, isEndCalendar ? endCalendarStartIndex + index : index))}</div></div>`; } } customElement("calcite-date-picker-month", DatePickerMonth); export { DatePickerMonth };