diff --git a/packages/main/cypress/specs/Calendar.cy.tsx b/packages/main/cypress/specs/Calendar.cy.tsx
index 47141ef325ef..cc967a909e5d 100644
--- a/packages/main/cypress/specs/Calendar.cy.tsx
+++ b/packages/main/cypress/specs/Calendar.cy.tsx
@@ -37,6 +37,19 @@ const getCalendarsWithWeekNumbers = () => (<>
>);
+const getCalendarWithDisabledDates = (id, formatPattern, ranges, props = {}) => (
+
+ {ranges.map((range, idx) => (
+
+ ))}
+
+);
+
describe("Calendar general interaction", () => {
it("Focus goes into the current day item of the day picker", () => {
const date = new Date(Date.UTC(2000, 10, 22, 0, 0, 0));
@@ -936,6 +949,157 @@ describe("Calendar general interaction", () => {
.should("have.length", 1);
});
+ it("Disabled date range prevents selection of dates within the range", () => {
+ cy.mount(getCalendarWithDisabledDates(
+ "calendar1",
+ "yyyy-MM-dd",
+ [{ startValue: "2024-11-10", endValue: "2024-11-15" }]
+ ));
+
+ const timestamp = new Date(Date.UTC(2024, 10, 10, 0, 0, 0)).valueOf() / 1000;
+ cy.get("#calendar1").invoke("prop", "timestamp", timestamp);
+
+ // Check that disabled dates have the correct class and aria-disabled attribute
+ const disabledDate = new Date(Date.UTC(2024, 10, 12, 0, 0, 0)).valueOf() / 1000;
+
+ cy.ui5CalendarGetDay("#calendar1", disabledDate.toString())
+ .should("have.class", "ui5-dp-item--disabled")
+ .should("have.attr", "aria-disabled", "true");
+
+ // Try to click on a disabled date
+ cy.ui5CalendarGetDay("#calendar1", disabledDate.toString())
+ .realClick();
+
+ // Verify the date was not selected
+ cy.ui5CalendarGetDay("#calendar1", disabledDate.toString())
+ .should("not.have.class", "ui5-dp-item--selected");
+ });
+
+ it("Disables a single date equal to start date when end date is not defined", () => {
+ cy.mount(getCalendarWithDisabledDates(
+ "calendar1",
+ "yyyy-MM-dd",
+ [{ startValue: "2024-11-15" }],
+ { maxDate: "2024-11-20" }
+ ));
+
+ const timestamp = new Date(Date.UTC(2024, 10, 10, 0, 0, 0)).valueOf() / 1000;
+ cy.get("#calendar1").invoke("prop", "timestamp", timestamp);
+
+ // Date before start should be enabled
+ const enabledDate = new Date(Date.UTC(2024, 10, 14, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", enabledDate.toString())
+ .should("not.have.class", "ui5-dp-item--disabled");
+
+ // Date at start should be disabled
+ const startDate = new Date(Date.UTC(2024, 10, 15, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", startDate.toString())
+ .should("have.class", "ui5-dp-item--disabled");
+
+ // Date after start should not be disabled
+ const afterStartDate = new Date(Date.UTC(2024, 10, 17, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", afterStartDate.toString())
+ .should("not.have.class", "ui5-dp-item--disabled");
+ });
+
+ it("Disables all dates before end date when start date is not defined", () => {
+ cy.mount(getCalendarWithDisabledDates(
+ "calendar1",
+ "yyyy-MM-dd",
+ [{ endValue: "2024-11-10" }],
+ { minDate: "2024-11-01" }
+ ));
+
+ const timestamp = new Date(Date.UTC(2024, 10, 10, 0, 0, 0)).valueOf() / 1000;
+ cy.get("#calendar1").invoke("prop", "timestamp", timestamp);
+
+ // Date after end should be enabled
+ const enabledDate = new Date(Date.UTC(2024, 10, 11, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", enabledDate.toString())
+ .should("not.have.class", "ui5-dp-item--disabled");
+
+ // Date at end should not be disabled
+ const endDate = new Date(Date.UTC(2024, 10, 10, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", endDate.toString())
+ .should("not.have.class", "ui5-dp-item--disabled");
+
+ // Date before end should be disabled
+ const beforeEndDate = new Date(Date.UTC(2024, 10, 8, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", beforeEndDate.toString())
+ .should("have.class", "ui5-dp-item--disabled");
+ });
+
+ it("Multiple disabled date ranges work correctly", () => {
+ cy.mount(getCalendarWithDisabledDates(
+ "calendar1",
+ "yyyy-MM-dd",
+ [
+ { startValue: "2024-11-05", endValue: "2024-11-07" },
+ { startValue: "2024-11-15", endValue: "2024-11-17" }
+ ]
+ ));
+
+ const timestamp = new Date(Date.UTC(2024, 10, 10, 0, 0, 0)).valueOf() / 1000;
+ cy.get("#calendar1").invoke("prop", "timestamp", timestamp);
+
+ // First range - should be disabled
+ const firstRangeDate = new Date(Date.UTC(2024, 10, 6, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", firstRangeDate.toString())
+ .should("have.class", "ui5-dp-item--disabled");
+
+ // Between ranges - should be enabled
+ const betweenDate = new Date(Date.UTC(2024, 10, 10, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", betweenDate.toString())
+ .should("not.have.class", "ui5-dp-item--disabled");
+
+ // Second range - should be disabled
+ const secondRangeDate = new Date(Date.UTC(2024, 10, 16, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", secondRangeDate.toString())
+ .should("have.class", "ui5-dp-item--disabled");
+ });
+
+ it("Disabled dates respect format pattern", () => {
+ cy.mount(getCalendarWithDisabledDates(
+ "calendar1",
+ "dd/MM/yyyy",
+ [{ startValue: "10/11/2024", endValue: "15/11/2024" }]
+ ));
+
+ const timestamp = new Date(Date.UTC(2024, 10, 10, 0, 0, 0)).valueOf() / 1000;
+ cy.get("#calendar1").invoke("prop", "timestamp", timestamp);
+
+ // Check disabled date
+ const disabledDate = new Date(Date.UTC(2024, 10, 12, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", disabledDate.toString())
+ .should("have.class", "ui5-dp-item--disabled");
+ });
+
+ it("Disabled dates work with range selection mode", () => {
+ cy.mount(getCalendarWithDisabledDates(
+ "calendar1",
+ "yyyy-MM-dd",
+ [{ startValue: "2024-11-10", endValue: "2024-11-15" }],
+ { selectionMode: "Range" }
+ ));
+
+ const timestamp = new Date(Date.UTC(2024, 10, 5, 0, 0, 0)).valueOf() / 1000;
+ cy.get("#calendar1").invoke("prop", "timestamp", timestamp);
+
+ // Try to select a range that includes disabled dates
+ const validStartDate = new Date(Date.UTC(2024, 10, 8, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", validStartDate.toString())
+ .realClick();
+
+ // Try to select an end date in the disabled range
+ const disabledEndDate = new Date(Date.UTC(2024, 10, 12, 0, 0, 0)).valueOf() / 1000;
+ cy.ui5CalendarGetDay("#calendar1", disabledEndDate.toString())
+ .realClick();
+
+ // Verify the date was not selected
+ cy.ui5CalendarGetDay("#calendar1", disabledEndDate.toString())
+ .should("not.have.class", "ui5-dp-item--selected");
+ });
+
it("Check calendar week numbers with specific CalendarWeekNumbering configuration", () => {
cy.mount(getCalendarsWithWeekNumbers());
diff --git a/packages/main/src/Calendar.ts b/packages/main/src/Calendar.ts
index ba14a1a2db4c..dcf6eae80d14 100644
--- a/packages/main/src/Calendar.ts
+++ b/packages/main/src/Calendar.ts
@@ -93,6 +93,11 @@ type CalendarYearRangeT = {
endYear: number,
}
+type DisabledDateRangeT = {
+ startValue?: string,
+ endValue?: string
+}
+
/**
* @class
*
@@ -327,6 +332,16 @@ class Calendar extends CalendarPart {
@slot({ type: HTMLElement, invalidateOnChildChange: true })
specialDates!: Array;
+ /**
+ * Defines the disabled date ranges that cannot be selected in the calendar.
+ * Use `ui5-date-range` elements to specify ranges of disabled dates.
+ * Each range can define a start date, an end date, or both.
+ * @public
+ * @since 2.16.0
+ */
+ @slot({ type: HTMLElement, invalidateOnChildChange: true })
+ disabledDates!: Array;
+
/**
* Defines the selected item type of the calendar legend item (if such exists).
* @private
@@ -431,6 +446,19 @@ class Calendar extends CalendarPart {
return !!date;
}
+ get _disabledDates() {
+ const validDisabledDateRanges = this.disabledDates.filter(dateRange => {
+ const startValue = dateRange.startValue;
+ const endValue = dateRange.endValue;
+ return (startValue && this._isValidCalendarDate(startValue)) || (endValue && this._isValidCalendarDate(endValue));
+ });
+
+ return validDisabledDateRanges.map(dateRange => ({
+ startValue: dateRange.startValue,
+ endValue: dateRange.endValue,
+ }));
+ }
+
get _specialCalendarDates() {
const hasSelectedType = this._specialDates.some(date => date.type === this._selectedItemType);
const validSpecialDates = this._specialDates.filter(date => {
@@ -963,4 +991,5 @@ export type {
ICalendarSelectedDates,
CalendarSelectionChangeEventDetail,
SpecialCalendarDateT,
+ DisabledDateRangeT,
};
diff --git a/packages/main/src/CalendarTemplate.tsx b/packages/main/src/CalendarTemplate.tsx
index dc96a6eccb24..edae4e593207 100644
--- a/packages/main/src/CalendarTemplate.tsx
+++ b/packages/main/src/CalendarTemplate.tsx
@@ -20,6 +20,7 @@ export default function CalendarTemplate(this: Calendar) {
formatPattern={this._formatPattern}
selectedDates={this._selectedDatesTimestamps}
specialCalendarDates={this._specialCalendarDates}
+ disabledDates={this._disabledDates}
_hidden={this._isDayPickerHidden}
primaryCalendarType={this._primaryCalendarType}
secondaryCalendarType={this._secondaryCalendarType}
diff --git a/packages/main/src/DayPicker.ts b/packages/main/src/DayPicker.ts
index c0306233661d..6b39305e959a 100644
--- a/packages/main/src/DayPicker.ts
+++ b/packages/main/src/DayPicker.ts
@@ -40,6 +40,7 @@ import DateFormat from "@ui5/webcomponents-localization/dist/DateFormat.js";
import CalendarSelectionMode from "./types/CalendarSelectionMode.js";
import CalendarPart from "./CalendarPart.js";
import type {
+ DisabledDateRangeT,
ICalendarPicker,
SpecialCalendarDateT,
} from "./Calendar.js";
@@ -196,6 +197,14 @@ class DayPicker extends CalendarPart implements ICalendarPicker {
@property({ type: Array })
specialCalendarDates: Array = [];
+ /**
+ * Array of disabled date ranges that cannot be selected.
+ * Each range can have a start and/or end date value.
+ * @private
+ */
+ @property({ type: Array })
+ disabledDates: Array = [];
+
@query("[data-sap-focus-ref]")
_focusableDay!: HTMLElement;
@@ -231,8 +240,6 @@ class DayPicker extends CalendarPart implements ICalendarPicker {
const tempDate = this._getFirstDay(); // date that will be changed by 1 day 42 times
const todayDate = CalendarDate.fromLocalJSDate(UI5Date.getInstance(), this._primaryCalendarType); // current day date - calculate once
const calendarDate = this._calendarDate; // store the _calendarDate value as this getter is expensive and degrades IE11 perf
- const minDate = this._minDate; // store the _minDate (expensive getter)
- const maxDate = this._maxDate; // store the _maxDate (expensive getter)
const tempSecondDate = this.hasSecondaryCalendarType ? this._getSecondaryDay(tempDate) : undefined;
@@ -255,7 +262,7 @@ class DayPicker extends CalendarPart implements ICalendarPicker {
const isSelectedBetween = this._isDayInsideSelectionRange(timestamp);
const isOtherMonth = tempDate.getMonth() !== calendarDate.getMonth();
const isWeekend = this._isWeekend(tempDate);
- const isDisabled = tempDate.valueOf() < minDate.valueOf() || tempDate.valueOf() > maxDate.valueOf();
+ const isDisabled = !this._isDateEnabled(tempDate);
const isToday = tempDate.isSame(todayDate);
const isFirstDayOfWeek = tempDate.getDay() === firstDayOfWeek;
@@ -818,6 +825,59 @@ class DayPicker extends CalendarPart implements ICalendarPicker {
|| (iWeekendEnd < iWeekendStart && (iWeekDay >= iWeekendStart || iWeekDay <= iWeekendEnd));
}
+ /**
+ * Checks if a given date is enabled (selectable).
+ * A date is considered disabled if:
+ * - It falls outside the min/max date range defined by the component
+ * - It matches a single disabled date
+ * - It falls within a disabled date range (exclusive of start and end dates)
+ * @param date - The date to check
+ * @returns `true` if the date is enabled (selectable), `false` if disabled
+ * @private
+ */
+ _isDateEnabled(date: CalendarDate): boolean {
+ if ((this._minDate && date.isBefore(this._minDate))
+ || (this._maxDate && date.isAfter(this._maxDate))) {
+ return false;
+ }
+
+ const dateTimestamp = date.valueOf() / 1000;
+
+ return !this.disabledDates.some(range => {
+ const startTimestamp = this._getTimestampFromDateValue(range.startValue);
+ const endTimestamp = this._getTimestampFromDateValue(range.endValue);
+
+ if (endTimestamp) {
+ return dateTimestamp > startTimestamp && dateTimestamp < endTimestamp;
+ }
+
+ return startTimestamp && dateTimestamp === startTimestamp;
+ });
+ }
+
+ /**
+ * Converts a date value string to a timestamp.
+ * @param dateValue - Date string to convert
+ * @returns timestamp in seconds, or 0 if invalid
+ * @private
+ */
+ _getTimestampFromDateValue(dateValue?: string): number {
+ if (!dateValue) {
+ return 0;
+ }
+
+ try {
+ const jsDate = this.getValueFormat().parse(dateValue) as Date;
+ const calendarDate = CalendarDate.fromLocalJSDate(
+ jsDate,
+ this._primaryCalendarType,
+ );
+ return calendarDate.valueOf() / 1000;
+ } catch {
+ return 0;
+ }
+ }
+
_isDayPressed(target: HTMLElement): boolean {
const targetParent = target.parentNode as HTMLElement;
return (target.className.indexOf("ui5-dp-item") > -1) || (targetParent && targetParent.classList && targetParent.classList.contains("ui5-dp-item"));
diff --git a/packages/main/test/pages/Calendar.html b/packages/main/test/pages/Calendar.html
index e088e99d0e72..656df0e4a9d2 100644
--- a/packages/main/test/pages/Calendar.html
+++ b/packages/main/test/pages/Calendar.html
@@ -86,7 +86,6 @@
-
Selection type for the first calendar:
@@ -133,6 +132,8 @@
+
+
Calendar with no format pattern & ISO min-max dates
@@ -178,6 +179,22 @@
+
+ Calendar with Disabled Dates
+
+
+
+
+
+
+
+ Calendar with Disabled Dates using a format pattern
+
+
+
+
+
+
+