Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1bcb6fa
feat
GDamyanov Nov 18, 2025
11564ac
feat: add sample in playground
GDamyanov Nov 19, 2025
8c7ad66
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 19, 2025
8c5a832
feat: update md file
GDamyanov Nov 19, 2025
33fef50
fix: lint errors
GDamyanov Nov 19, 2025
2e28efc
refactor: revert change
GDamyanov Nov 19, 2025
fc93ee4
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 19, 2025
343304a
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 19, 2025
68f9479
refactor: update method name
GDamyanov Nov 19, 2025
284dc14
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 19, 2025
71a2180
Update packages/main/cypress/specs/Calendar.cy.tsx
GDamyanov Nov 20, 2025
51bb17c
Update packages/main/cypress/specs/Calendar.cy.tsx
GDamyanov Nov 20, 2025
57e74a9
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 20, 2025
d9960c7
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 20, 2025
8d66860
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 21, 2025
6d4c853
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 24, 2025
138dc54
refactor: refactor tests
GDamyanov Nov 24, 2025
0c444cb
refactor: use utils methods
GDamyanov Nov 25, 2025
a62fe8b
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 25, 2025
fff4dfe
feat: use utils
GDamyanov Nov 25, 2025
41899dd
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 25, 2025
666825c
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 25, 2025
f30b9e4
fix: update samples
GDamyanov Nov 25, 2025
e143612
Update packages/main/test/pages/Calendar.html
GDamyanov Nov 25, 2025
555c611
refactor: update jsdoc
GDamyanov Nov 25, 2025
5decd79
test: refactor tests
GDamyanov Nov 26, 2025
9d30dc5
refactor: remove redundant getter
GDamyanov Nov 26, 2025
e601fb8
refactor: optimise code
GDamyanov Nov 26, 2025
3e02970
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 26, 2025
058b171
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 27, 2025
7da49e3
Merge branch 'main' into calendar-disabled-dates
GDamyanov Nov 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 164 additions & 0 deletions packages/main/cypress/specs/Calendar.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ const getCalendarsWithWeekNumbers = () => (<>
</Calendar>
</>);

const getCalendarWithDisabledDates = (id, formatPattern, ranges, props = {}) => (
<Calendar id={id} formatPattern={formatPattern} {...props}>
{ranges.map((range, idx) => (
<CalendarDateRange
slot="disabledDates"
key={idx}
startValue={range.startValue}
endValue={range.endValue}
/>
))}
</Calendar>
);

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));
Expand Down Expand Up @@ -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<Calendar>("#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<Calendar>("#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<Calendar>("#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<Calendar>("#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<Calendar>("#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<Calendar>("#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());

Expand Down
29 changes: 29 additions & 0 deletions packages/main/src/Calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ type CalendarYearRangeT = {
endYear: number,
}

type DisabledDateRangeT = {
startValue?: string,
endValue?: string
}

/**
* @class
*
Expand Down Expand Up @@ -327,6 +332,16 @@ class Calendar extends CalendarPart {
@slot({ type: HTMLElement, invalidateOnChildChange: true })
specialDates!: Array<SpecialCalendarDate>;

/**
* 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<CalendarDateRange>;

/**
* Defines the selected item type of the calendar legend item (if such exists).
* @private
Expand Down Expand Up @@ -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 => {
Expand Down Expand Up @@ -963,4 +991,5 @@ export type {
ICalendarSelectedDates,
CalendarSelectionChangeEventDetail,
SpecialCalendarDateT,
DisabledDateRangeT,
};
1 change: 1 addition & 0 deletions packages/main/src/CalendarTemplate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
66 changes: 63 additions & 3 deletions packages/main/src/DayPicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -196,6 +197,14 @@ class DayPicker extends CalendarPart implements ICalendarPicker {
@property({ type: Array })
specialCalendarDates: Array<SpecialCalendarDateT> = [];

/**
* 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<DisabledDateRangeT> = [];

@query("[data-sap-focus-ref]")
_focusableDay!: HTMLElement;

Expand Down Expand Up @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we sure that either of the methods throw an error to be caught?

If not we can just go for:

const jsDate = this.getValueFormat().parse(dateValue);
const calendarDate = CalendarDate.fromLocalJSDate(
jsDate,
this._primaryCalendarType,
);
return calendarDate.valueOf() / 1000;

And if needed to check if there's calendarDate before returning it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fromLocalJSDate method throws exception.

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"));
Expand Down
19 changes: 18 additions & 1 deletion packages/main/test/pages/Calendar.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@
</style>
</head>
<body class="calendar1auto">

<section>
<ui5-label id="selectLabel" label-for="selectionType">Selection type for the first calendar:</ui5-label>
<ui5-select id="selectionType">
Expand Down Expand Up @@ -133,6 +132,8 @@
</ui5-calendar>
</section>



<section>
<ui5-title> Calendar with no format pattern & ISO min-max dates</ui5-title>
<ui5-calendar id="calendar6" min-date="2020-10-20" max-date="2023-10-20"></ui5-calendar>
Expand Down Expand Up @@ -178,6 +179,22 @@
<ui5-calendar class="header-parts-demo"></ui5-calendar>
</section>

<section>
<ui5-title>Calendar with Disabled Dates</ui5-title>
<ui5-calendar id="calendar-disabled-dates">
<ui5-date-range slot="disabledDates" start-value="15 Nov 2025" end-value="20 Nov 2025"></ui5-date-range>
<ui5-date-range slot="disabledDates" start-value="25 Nov 2025" end-value="27 Nov 2025"></ui5-date-range>
</ui5-calendar>
</section>

<section>
<ui5-title>Calendar with Disabled Dates using a format pattern</ui5-title>
<ui5-calendar id="calendar-disabled-dates" format-pattern="dd/MM/yyyy">
<ui5-date-range slot="disabledDates" start-value="15/11/2025" end-value="20/11/2025"></ui5-date-range>
<ui5-date-range slot="disabledDates" start-value="25/11/2025" end-value="27/11/2025"></ui5-date-range>
</ui5-calendar>
</section>

</body>

<script>
Expand Down
Loading
Loading