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();