diff --git a/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts b/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts index 8e216ecf033..38c2e658027 100644 --- a/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts +++ b/packages/calcite-components/src/components/date-picker/date-picker.e2e.ts @@ -1,6 +1,7 @@ // @ts-strict-ignore import { E2EElement, E2EPage, newE2EPage } from "@arcgis/lumina-compiler/puppeteerTesting"; import { describe, expect, it } from "vitest"; +import { ConditionalPick } from "type-fest"; import { html } from "../../../support/formatting"; import { defaults, focusable, hidden, renders, t9n } from "../../tests/commonTests"; import { findAll, skipAnimations } from "../../tests/utils/puppeteer"; @@ -235,7 +236,6 @@ describe("calcite-date-picker", () => { it("unsetting min/max updates minAsDate & maxAsDate", async () => { const page = await newE2EPage(); - await page.emulateTimezone("America/Los_Angeles"); await page.setContent( html``, ); @@ -249,9 +249,9 @@ describe("calcite-date-picker", () => { expect(await element.getProperty("minAsDate")).toBe(undefined); expect(await element.getProperty("maxAsDate")).toBe(undefined); - const dateBeyondMax = "2022-11-26"; - await setActiveDate(page, dateBeyondMax); - expect(await getActiveDate(page)).toEqual(new Date(dateBeyondMax).toISOString()); + const dateAfterMax = "2022-11-26"; + await setActiveDate(page, dateAfterMax); + expect(await getActiveDate(page)).toEqual(new Date(dateAfterMax).toISOString()); const dateBeforeMin = "2022-11-14"; await setActiveDate(page, dateBeforeMin); @@ -279,6 +279,42 @@ describe("calcite-date-picker", () => { expect(await monthOptions[3].getProperty("disabled")).toBe(false); expect(await monthOptions[4].getProperty("disabled")).toBe(true); }); + + it("disables days outside minAsDate and maxAsDate", async () => { + const page = await newE2EPage(); + await page.setContent(html``); + + const beforeMinDateOnlyIso = "2025-08-17"; + const minDateOnlyIso = "2025-08-18"; + const maxDateOnlyIso = "2025-08-20"; + const afterMaxDateOnlyIso = "2025-08-21"; + const offsetIso = "T07:00:00.000Z"; + const minDayIso = `${minDateOnlyIso}${offsetIso}`; + const maxDayIso = `${maxDateOnlyIso}${offsetIso}`; + + await page.$eval( + "calcite-date-picker", + (el, min, max) => { + el.minAsDate = new Date(min); + el.maxAsDate = new Date(max); + }, + minDayIso, + maxDayIso, + ); + await page.waitForChanges(); + + const previousDay = await getDayById(page, dateIsoToDayId(beforeMinDateOnlyIso)); + expect(await previousDay.getProperty("disabled")).toBe(true); + + const currentDay = await getDayById(page, dateIsoToDayId(minDateOnlyIso)); + expect(await currentDay.getProperty("disabled")).toBe(false); + + const maxDay = await getDayById(page, dateIsoToDayId(maxDateOnlyIso)); + expect(await maxDay.getProperty("disabled")).toBe(false); + + const outOfRangeDay = await getDayById(page, dateIsoToDayId(afterMaxDateOnlyIso)); + expect(await outOfRangeDay.getProperty("disabled")).toBe(true); + }); }); describe("translation support", () => { @@ -835,10 +871,10 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); const datePicker = await page.find("calcite-date-picker"); - const currentDate = new Date(); + const currentDate = getLocalDayDate(); currentDate.setMonth(currentDate.getMonth() + 2); currentDate.setDate(12); - const currentISODate = currentDate.toISOString().split("T")[0]; + const currentISODate = toDateOnlyIso(currentDate); await page.evaluate((currentISODate) => { const datePicker = document.querySelector("calcite-date-picker"); @@ -861,21 +897,23 @@ describe("calcite-date-picker", () => { it("should select current day when min is before current day but in same month of range date-picker", async () => { const page = await newE2EPage(); await page.setContent(html``); - await page.waitForChanges(); const datePicker = await page.find("calcite-date-picker"); - const currentDate = new Date(); + const currentDate = getLocalDayDate(); if (currentDate.getDate() > 2) { currentDate.setDate(1); } - const currentISODate = currentDate.toISOString().split("T")[0]; - - await page.evaluate((currentISODate) => { - const datePicker = document.querySelector("calcite-date-picker"); - datePicker.min = currentISODate; - }, currentISODate); + const currentISODate = toDateOnlyIso(currentDate); + await page.$eval( + "calcite-date-picker", + (datePicker, currentISODate) => { + datePicker.min = currentISODate; + }, + currentISODate, + ); await page.waitForChanges(); + await page.keyboard.press("Tab"); await page.waitForChanges(); await page.keyboard.press("Tab"); @@ -885,8 +923,8 @@ describe("calcite-date-picker", () => { await page.keyboard.press("Enter"); await page.waitForChanges(); - const currentDayDate = new Date(); - const currentDayISODate = currentDayDate.toISOString().split("T")[0]; + const currentDayDate = getLocalDayDate(); + const currentDayISODate = toDateOnlyIso(currentDayDate); expect(await datePicker.getProperty("value")).toStrictEqual([currentDayISODate, ""]); }); @@ -894,21 +932,16 @@ describe("calcite-date-picker", () => { it("should select current day when min is before current day but in same month", async () => { const page = await newE2EPage(); await page.setContent(html``); - await page.waitForChanges(); const datePicker = await page.find("calcite-date-picker"); - const currentDate = new Date(); - if (currentDate.getDate() > 2) { - currentDate.setDate(1); + const minDate = getLocalDayDate(); + if (minDate.getDate() > 2) { + minDate.setDate(1); } - const currentISODate = currentDate.toISOString().split("T")[0]; - - await page.evaluate((currentISODate) => { - const datePicker = document.querySelector("calcite-date-picker"); - datePicker.min = currentISODate; - }, currentISODate); + datePicker.setProperty("min", toDateOnlyIso(minDate)); await page.waitForChanges(); + await page.keyboard.press("Tab"); await page.waitForChanges(); await page.keyboard.press("Tab"); @@ -920,8 +953,8 @@ describe("calcite-date-picker", () => { await page.keyboard.press("Enter"); await page.waitForChanges(); - const currentDayDate = new Date(); - const currentDayISODate = currentDayDate.toISOString().split("T")[0]; + const currentDayDate = getLocalDayDate(); + const currentDayISODate = toDateOnlyIso(currentDayDate); expect(await datePicker.getProperty("value")).toEqual(currentDayISODate); }); @@ -932,10 +965,10 @@ describe("calcite-date-picker", () => { await page.waitForChanges(); const datePicker = await page.find("calcite-date-picker"); - const currentDate = new Date(); + const currentDate = getLocalDayDate(); currentDate.setMonth(currentDate.getMonth() + 2); currentDate.setDate(12); - const currentISODate = currentDate.toISOString().split("T")[0]; + const currentISODate = toDateOnlyIso(currentDate); await page.evaluate((currentISODate) => { const datePicker = document.querySelector("calcite-date-picker"); @@ -1204,63 +1237,81 @@ describe("calcite-date-picker", () => { themed(rangeDatePickerHTML, componentTokens); }); }); -}); -async function setActiveDate(page: E2EPage, date: string): Promise { - await page.evaluate((date) => { - const datePicker = document.querySelector("calcite-date-picker"); - datePicker.activeDate = new Date(date); - }, date); - await page.waitForChanges(); -} - -async function getActiveDate(page: E2EPage): Promise { - return await page.evaluate(() => { - const datePicker = document.querySelector("calcite-date-picker"); - return datePicker.activeDate.toISOString(); - }); -} - -async function selectDayInMonthById(id: string, page: E2EPage): Promise { - const day = await page.find( - `calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[current-month][id="${id}"]`, - ); - await day.click(); - await page.waitForChanges(); -} - -async function selectFirstAvailableDay(page: E2EPage): Promise { - const day = await page.find( - "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day:not([selected])", - ); - await day.click(); - await page.waitForChanges(); -} - -async function selectSelectedDay(page: E2EPage): Promise { - const day = await page.find( - "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[selected]", - ); - await day.click(); - await page.waitForChanges(); -} - -async function getDayById(page: E2EPage, id: string): Promise { - const days = await findAll( - page, - `calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[id="${id}"]`, - ); - return days.find((d) => !d.classList.contains("noncurrent")); -} - -async function getActiveMonth(page: E2EPage, position: Extract<"start" | "end", Position> = "start"): Promise { - const [startMonth, endMonth] = await findAll( - page, - `calcite-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header} >>> calcite-select.${MONTH_HEADER_CSS.monthPicker}`, - ); - - if (position === "start") { - return (await startMonth.find("calcite-option[selected]")).textContent; + function getLocalDayDate(): Date { + const today = new Date(); + today.setMinutes(today.getMinutes() - today.getTimezoneOffset()); + return today; + } + + function toDateOnlyIso(date: Date): string { + return date.toISOString().split("T")[0]; + } + + async function setActiveDate(page: E2EPage, isoDate: string): Promise { + await page.$eval("calcite-date-picker", (datePicker, date) => (datePicker.activeDate = new Date(date)), isoDate); + await page.waitForChanges(); + } + + async function getActiveDate(page: E2EPage): Promise { + return getDateIsoStringFromProp(page, "activeDate"); + } + + async function getDateIsoStringFromProp( + page: E2EPage, + prop: keyof ConditionalPick, + ): Promise { + return page.$eval("calcite-date-picker", (datePicker, prop) => datePicker[prop]?.toISOString(), prop); + } + + async function selectDayInMonthById(id: string, page: E2EPage): Promise { + const day = await page.find( + `calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[current-month][id="${id}"]`, + ); + await day.click(); + await page.waitForChanges(); + } + + async function selectFirstAvailableDay(page: E2EPage): Promise { + const day = await page.find( + "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day:not([selected])", + ); + await day.click(); + await page.waitForChanges(); } - return (await endMonth.find("calcite-option[selected]")).textContent; -} + + async function selectSelectedDay(page: E2EPage): Promise { + const day = await page.find( + "calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[selected]", + ); + await day.click(); + await page.waitForChanges(); + } + + function dateIsoToDayId(isoDate: string): string { + return isoDate.split("T")[0].replaceAll("-", ""); + } + + async function getDayById(page: E2EPage, id: string): Promise { + const days = await findAll( + page, + `calcite-date-picker >>> calcite-date-picker-month >>> calcite-date-picker-day[id="${id}"]`, + ); + return days.find((d) => !d.classList.contains("noncurrent")); + } + + async function getActiveMonth( + page: E2EPage, + position: Extract<"start" | "end", Position> = "start", + ): Promise { + const [startMonth, endMonth] = await findAll( + page, + `calcite-date-picker >>> calcite-date-picker-month-header >>> .${MONTH_HEADER_CSS.header} >>> calcite-select.${MONTH_HEADER_CSS.monthPicker}`, + ); + + if (position === "start") { + return (await startMonth.find("calcite-option[selected]")).textContent; + } + return (await endMonth.find("calcite-option[selected]")).textContent; + } +}); diff --git a/packages/calcite-components/src/components/date-picker/date-picker.tsx b/packages/calcite-components/src/components/date-picker/date-picker.tsx index bd4c9800507..239299a337d 100644 --- a/packages/calcite-components/src/components/date-picker/date-picker.tsx +++ b/packages/calcite-components/src/components/date-picker/date-picker.tsx @@ -1,14 +1,14 @@ // @ts-strict-ignore -import { PropertyValues, isServer } from "lit"; +import { isServer, PropertyValues } from "lit"; import { - LitElement, - property, createEvent, Fragment, h, + JsxNode, + LitElement, method, + property, state, - JsxNode, } from "@arcgis/lumina"; import { dateFromISO, @@ -27,7 +27,7 @@ import { HeadingLevel } from "../functional/Heading"; import { useT9n } from "../../controllers/useT9n"; import { useSetFocus } from "../../controllers/useSetFocus"; import T9nStrings from "./assets/t9n/messages.en.json"; -import { DATE_PICKER_FORMAT_OPTIONS, HEADING_LEVEL, CSS } from "./resources"; +import { CSS, DATE_PICKER_FORMAT_OPTIONS, HEADING_LEVEL } from "./resources"; import { DateLocaleData, getLocaleData, getValueAsDateRange } from "./utils"; import { styles } from "./date-picker.scss"; @@ -188,34 +188,11 @@ export class DatePicker extends LitElement { this.listen("keydown", this.keyDownHandler); } - override connectedCallback(): void { - if (Array.isArray(this.value)) { - this.valueAsDate = getValueAsDateRange(this.value); - } else if (this.value) { - this.valueAsDate = dateFromISO(this.value); - } - - if (this.min) { - this.minAsDate = dateFromISO(this.min); - } - - if (this.max) { - this.maxAsDate = dateFromISO(this.max); - } - this.setActiveStartAndEndDates(); - } - async load(): Promise { await this.loadLocaleData(); - this.onMinChanged(this.min); - this.onMaxChanged(this.max); } override willUpdate(changes: PropertyValues): void { - if (changes.has("activeDate")) { - this.activeDateWatcher(this.activeDate); - } - if (changes.has("value")) { this.valueHandler(this.value); } @@ -224,12 +201,43 @@ export class DatePicker extends LitElement { this.valueAsDateWatcher(this.valueAsDate); } - if (changes.has("min")) { - this.onMinChanged(this.min); + let minSource: Extract; + let maxSource: Extract; + + if (changes.has("min") && !changes.has("minAsDate")) { + minSource = "min"; + } else if (changes.has("minAsDate") && !changes.has("min")) { + minSource = "minAsDate"; } - if (changes.has("max")) { - this.onMaxChanged(this.max); + if (changes.has("max") && !changes.has("maxAsDate")) { + maxSource = "max"; + } else if (changes.has("maxAsDate") && !changes.has("max")) { + maxSource = "maxAsDate"; + } + + if (minSource === "min") { + this.minAsDate = dateFromISO(this.min); + } else if (minSource === "minAsDate") { + this.minAsDate = dateFromISO(dateToISO(this.minAsDate)); + } + + if (maxSource === "max") { + this.maxAsDate = dateFromISO(this.max); + } else if (maxSource === "maxAsDate") { + this.maxAsDate = dateFromISO(dateToISO(this.maxAsDate)); + } + + if ( + (changes.has("range") && this.range) || + changes.has("maxAsDate") || + changes.has("minAsDate") + ) { + this.setActiveStartAndEndDates(); + } + + if (changes.has("activeDate")) { + this.activeDateWatcher(this.activeDate); } if (changes.has("messages") && this.hasUpdated) { @@ -275,20 +283,6 @@ export class DatePicker extends LitElement { } } - private onMinChanged(min: string): void { - this.minAsDate = dateFromISO(min); - if (this.range) { - this.setActiveStartAndEndDates(); - } - } - - private onMaxChanged(max: string): void { - this.maxAsDate = dateFromISO(max); - if (this.range) { - this.setActiveStartAndEndDates(); - } - } - private keyDownHandler(event: KeyboardEvent): void { if (event.key === "Escape") { this.resetActiveDates();