Skip to content

Commit 336c20a

Browse files
committed
fix(calendar): Clamp date arithmetic rollovers
`CalendarDay.set` now does an additional check when date rollover occurs, clamping the value to the last date of the passed month. As a side note, when will `Temporal` be baseline available. Closes #1710
1 parent c0d9e41 commit 336c20a

File tree

4 files changed

+68
-17
lines changed

4 files changed

+68
-17
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](http://keepachangelog.com/)
55
and this project adheres to [Semantic Versioning](http://semver.org/).
66

7+
## [Unreleased]
8+
### Fixed
9+
- #### Calendar & Date picker
10+
- Incorrect date rollover for in certain scenarios [#1710](https://github.com/IgniteUI/igniteui-webcomponents/issues/1710)
11+
712
## [6.0.1] - 2025-05-28
813
### Added
914
- #### Radio group

src/components/calendar/model.spec.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -127,20 +127,15 @@ describe('Calendar day model', () => {
127127
const nonLeapFebruary = leapFebruary.set({ year: 2023 });
128128
let { year, month, date } = nonLeapFebruary;
129129

130-
// Shift to first day of next month -> 2024/03/01
131-
expect([year, month, date]).to.eql([2023, 2, 1]);
132-
133-
const lastDayOfJuly = new CalendarDay({
134-
year: 2024,
135-
month: 6,
136-
date: 31,
137-
});
130+
// Shift to last day of the current month -> 2023-02-28
131+
expect([year, month, date]).to.eql([2023, 1, 28]);
138132

133+
const lastDayOfJuly = new CalendarDay({ year: 2024, month: 6, date: 31 });
139134
const lastDayOfApril = lastDayOfJuly.set({ month: 3 });
140135
({ year, month, date } = lastDayOfApril);
141136

142-
// April does not have 31 days so shift to first day of May
143-
expect([year, month, date]).to.eql([2024, 4, 1]);
137+
// April does not have 31 days so clamp to the last day of April
138+
expect([year, month, date]).to.eql([2024, 3, 30]);
144139
});
145140
});
146141

src/components/calendar/model.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function checkRollover(original: CalendarDay, modified: CalendarDay) {
2727

2828
/* blazorSuppress */
2929
export class CalendarDay {
30-
private _date!: Date;
30+
private readonly _date: Date;
3131

3232
/** Constructs and returns the current day. */
3333
public static get today() {
@@ -56,11 +56,21 @@ export class CalendarDay {
5656
* Returns a new instance with values replaced.
5757
*/
5858
public set(args: Partial<CalendarDayParams>) {
59-
return new CalendarDay({
60-
year: args.year ?? this.year,
61-
month: args.month ?? this.month,
62-
date: args.date ?? this.date,
63-
});
59+
const year = args.year ?? this.year;
60+
const month = args.month ?? this.month;
61+
const date = args.date ?? this.date;
62+
63+
const temp = new Date(year, month, date);
64+
65+
if (date > 0 && temp.getMonth() !== month) {
66+
return new CalendarDay({
67+
year,
68+
month,
69+
date: new Date(year, month + 1, 0).getDate(),
70+
});
71+
}
72+
73+
return new CalendarDay({ year, month, date });
6474
}
6575

6676
public add(unit: DayInterval, value: number) {

src/components/date-picker/date-picker.spec.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { elementUpdated, expect, fixture, html } from '@open-wc/testing';
22
import { spy } from 'sinon';
33
import IgcCalendarComponent from '../calendar/calendar.js';
4-
import { getCalendarDOM, getDayViewDOM } from '../calendar/helpers.spec.js';
4+
import {
5+
getCalendarDOM,
6+
getDOMDate,
7+
getDayViewDOM,
8+
} from '../calendar/helpers.spec.js';
59
import { CalendarDay, toCalendarDay } from '../calendar/model.js';
610
import { DateRangeType } from '../calendar/types.js';
711
import {
@@ -897,6 +901,43 @@ describe('Date picker', () => {
897901
expect(picker.open).to.be.true;
898902
});
899903

904+
it('issue 1710', async () => {
905+
const activeDate = new CalendarDay({ year: 2025, month: 4, date: 29 });
906+
const targetDate = activeDate.add('day', 2);
907+
908+
// Select the last date of May
909+
picker.activeDate = activeDate.native;
910+
await picker.show();
911+
912+
const calendarDOM = getCalendarDOM(calendar);
913+
const lastOfMay = getDOMDate(targetDate, calendarDOM.views.days);
914+
915+
simulateClick(lastOfMay);
916+
await elementUpdated(picker);
917+
918+
expect(checkDatesEqual(picker.value!, targetDate));
919+
920+
// Open the picker and switch to months view
921+
await picker.show();
922+
923+
simulateClick(calendarDOM.navigation.months);
924+
await elementUpdated(picker);
925+
926+
const monthElements = Array.from(
927+
calendarDOM.views.months.renderRoot.querySelectorAll<HTMLElement>(
928+
'[role="gridcell"]'
929+
)
930+
);
931+
932+
const monthNames = new Set(monthElements.map((each) => each.innerText));
933+
const selectedMonths = monthElements.filter((each) =>
934+
each.matches('[aria-selected="true"]')
935+
);
936+
937+
expect(monthNames).lengthOf(12);
938+
expect(selectedMonths).lengthOf(1);
939+
});
940+
900941
describe('Readonly state', () => {
901942
describe('Dropdown mode', () => {
902943
beforeEach(async () => {

0 commit comments

Comments
 (0)