Skip to content

Commit c202731

Browse files
committed
fix: fix normalization and add tests
1 parent 37a82c0 commit c202731

File tree

2 files changed

+174
-18
lines changed

2 files changed

+174
-18
lines changed

packages/devextreme/js/__internal/scheduler/__tests__/workspace.test.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ describe('Workspace', () => {
8080
});
8181
});
8282

83-
describe('scrollTo (T1310544)', () => {
83+
describe('scrollTo', () => {
8484
beforeEach(() => {
8585
fx.off = true;
8686
setupSchedulerTestEnvironment({ height: 600 });
@@ -125,5 +125,109 @@ describe('Workspace', () => {
125125

126126
scrollBySpy.mockRestore();
127127
});
128+
129+
describe('hour normalization', () => {
130+
it('should normalize hours to visible range without viewOffset', async () => {
131+
const { scheduler } = await createScheduler({
132+
views: ['timelineDay'],
133+
currentView: 'timelineDay',
134+
currentDate: new Date(2021, 1, 2),
135+
startDayHour: 6,
136+
endDayHour: 18,
137+
offset: 0,
138+
});
139+
140+
const workspace = scheduler.getWorkSpace();
141+
const scrollable = workspace.getScrollable();
142+
const scrollBySpy = jest.spyOn(scrollable, 'scrollBy');
143+
144+
// Below startDayHour (6), should normalize to 6
145+
const dateBelowRange = new Date(2021, 1, 2, 4, 0);
146+
scheduler.scrollTo(dateBelowRange, undefined, false);
147+
expect(scrollBySpy).toHaveBeenCalled();
148+
149+
scrollBySpy.mockClear();
150+
// Above endDayHour (18), should normalize to 17
151+
const dateAboveRange = new Date(2021, 1, 2, 20, 0);
152+
scheduler.scrollTo(dateAboveRange, undefined, false);
153+
expect(scrollBySpy).toHaveBeenCalled();
154+
155+
scrollBySpy.mockClear();
156+
// Within range [6, 18), should scroll normally
157+
const dateInRange = new Date(2021, 1, 2, 12, 0);
158+
scheduler.scrollTo(dateInRange, undefined, false);
159+
expect(scrollBySpy).toHaveBeenCalled();
160+
161+
scrollBySpy.mockRestore();
162+
});
163+
164+
it('should normalize hours to visible range with viewOffset (no midnight crossing)', async () => {
165+
const { scheduler } = await createScheduler({
166+
views: ['timelineDay'],
167+
currentView: 'timelineDay',
168+
currentDate: new Date(2021, 1, 2),
169+
startDayHour: 6,
170+
endDayHour: 18,
171+
offset: 360,
172+
});
173+
174+
const workspace = scheduler.getWorkSpace();
175+
const scrollable = workspace.getScrollable();
176+
const scrollBySpy = jest.spyOn(scrollable, 'scrollBy');
177+
178+
// Below adjustedStartDayHour (12), should normalize to 12
179+
const dateBelowAdjustedRange = new Date(2021, 1, 2, 10, 0);
180+
scheduler.scrollTo(dateBelowAdjustedRange, undefined, false);
181+
expect(scrollBySpy).toHaveBeenCalled();
182+
183+
scrollBySpy.mockClear();
184+
// Within adjusted range [12, 24), should scroll normally
185+
const dateInAdjustedRange = new Date(2021, 1, 2, 15, 0);
186+
scheduler.scrollTo(dateInAdjustedRange, undefined, false);
187+
expect(scrollBySpy).toHaveBeenCalled();
188+
189+
scrollBySpy.mockRestore();
190+
});
191+
192+
it('should normalize hours to visible range with viewOffset (midnight crossing)', async () => {
193+
const { scheduler } = await createScheduler({
194+
views: ['timelineDay'],
195+
currentView: 'timelineDay',
196+
currentDate: new Date(2021, 1, 2),
197+
startDayHour: 6,
198+
endDayHour: 18,
199+
offset: 720,
200+
});
201+
202+
const workspace = scheduler.getWorkSpace();
203+
const scrollable = workspace.getScrollable();
204+
const scrollBySpy = jest.spyOn(scrollable, 'scrollBy');
205+
206+
// In gap [6, 18), should normalize to 18:00 Feb 2
207+
const dateInGap = new Date(2021, 1, 2, 10, 0);
208+
scheduler.scrollTo(dateInGap, undefined, false);
209+
expect(scrollBySpy).toHaveBeenCalled();
210+
211+
scrollBySpy.mockClear();
212+
// In range [18, 24) on Feb 2, should scroll normally
213+
const dateInFirstRange = new Date(2021, 1, 2, 22, 0);
214+
scheduler.scrollTo(dateInFirstRange, undefined, false);
215+
expect(scrollBySpy).toHaveBeenCalled();
216+
217+
scrollBySpy.mockClear();
218+
// In range [0, 6) but on wrong day (Feb 2), should normalize to 18:00 Feb 2
219+
const dateInSecondRangeWrongDay = new Date(2021, 1, 2, 3, 0);
220+
scheduler.scrollTo(dateInSecondRangeWrongDay, undefined, false);
221+
expect(scrollBySpy).toHaveBeenCalled();
222+
223+
scrollBySpy.mockClear();
224+
// In range [0, 6) on correct day (Feb 3), should scroll normally
225+
const dateInSecondRangeCorrectDay = new Date(2021, 1, 3, 3, 0);
226+
scheduler.scrollTo(dateInSecondRangeCorrectDay, undefined, false);
227+
expect(scrollBySpy).toHaveBeenCalled();
228+
229+
scrollBySpy.mockRestore();
230+
});
231+
});
128232
});
129233
});

packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,30 +1407,68 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {
14071407
return (this.$element() as any).find(`.${GROUP_HEADER_CLASS}`);
14081408
}
14091409

1410-
_getScrollCoordinates(hours, minutes, date, groupIndex?: any, allDay?: any) {
1411-
const currentDate = date || new Date(this.option('currentDate'));
1410+
_getAdjustedHourBoundaries() {
14121411
const startDayHour = this.option('startDayHour');
14131412
const endDayHour = this.option('endDayHour');
1414-
const viewOffset = this.option('viewOffset');
1413+
const viewOffset = this.option('viewOffset') as number;
1414+
const viewOffsetHours = viewOffset / HOUR_MS;
1415+
const adjustedStartDayHour = (startDayHour + viewOffsetHours) % 24;
1416+
const adjustedEndDayHour = (endDayHour + viewOffsetHours) % 24;
1417+
const crossesMidnight = adjustedStartDayHour >= adjustedEndDayHour;
14151418

1416-
if (viewOffset === 0) {
1417-
if (hours < startDayHour) {
1418-
hours = startDayHour;
1419-
}
1419+
return { adjustedStartDayHour, adjustedEndDayHour, crossesMidnight };
1420+
}
14201421

1421-
if (hours >= endDayHour) {
1422-
hours = endDayHour - 1;
1422+
_normalizeHoursForScroll(hours: number, date: Date): { normalizedHours: number; normalizedDate: Date } {
1423+
const min = this.getStartViewDate();
1424+
const { adjustedStartDayHour, adjustedEndDayHour, crossesMidnight } = this._getAdjustedHourBoundaries();
1425+
const dateDayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate());
1426+
const minDayStart = new Date(min.getFullYear(), min.getMonth(), min.getDate());
1427+
const nextDay = new Date(minDayStart);
1428+
nextDay.setDate(nextDay.getDate() + 1);
1429+
1430+
let normalizedDate: Date;
1431+
let normalizedHours = hours;
1432+
1433+
if (crossesMidnight) {
1434+
// Boundaries cross midnight: [adjustedStartDayHour, 24) on minDay ∪ [0, adjustedEndDayHour) on next day
1435+
const isOnMinDay = dateDayStart.getTime() === minDayStart.getTime();
1436+
const isOnNextDay = dateDayStart.getTime() === nextDay.getTime();
1437+
1438+
if (hours >= adjustedStartDayHour && isOnMinDay) {
1439+
// Valid: hours [adjustedStartDayHour, 24) on minDay
1440+
normalizedDate = new Date(minDayStart);
1441+
} else if (hours < adjustedEndDayHour && isOnNextDay) {
1442+
// Valid: hours [0, adjustedEndDayHour) on next day
1443+
normalizedDate = new Date(nextDay);
1444+
} else {
1445+
// Normalize to adjustedStartDayHour on minDay
1446+
normalizedDate = new Date(minDayStart);
1447+
normalizedHours = adjustedStartDayHour;
1448+
}
1449+
} else {
1450+
// Normal case: boundaries don't cross midnight
1451+
normalizedDate = new Date(minDayStart);
1452+
if (hours < adjustedStartDayHour) {
1453+
normalizedHours = adjustedStartDayHour;
1454+
} else if (hours >= adjustedEndDayHour) {
1455+
normalizedHours = adjustedEndDayHour - 1;
14231456
}
14241457
}
14251458

1426-
currentDate.setHours(hours, minutes, 0, 0);
1459+
return { normalizedHours, normalizedDate };
1460+
}
1461+
1462+
_getScrollCoordinates(hours: number, minutes: number, date: Date, groupIndex?: number, allDay?: boolean) {
1463+
const { normalizedHours, normalizedDate } = this._normalizeHoursForScroll(hours, date);
1464+
normalizedDate.setHours(normalizedHours, minutes, 0, 0);
14271465

1428-
const cell = this.viewDataProvider.findGlobalCellPosition(currentDate, groupIndex, allDay);
1466+
const cell = this.viewDataProvider.findGlobalCellPosition(normalizedDate, groupIndex, allDay);
14291467

14301468
return this.virtualScrollingDispatcher.calculateCoordinatesByDataAndPosition(
14311469
cell?.cellData,
14321470
cell?.position,
1433-
currentDate,
1471+
normalizedDate,
14341472
isDateAndTimeView(this.type as any),
14351473
this.viewDirection === 'vertical',
14361474
);
@@ -1868,14 +1906,28 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {
18681906
}
18691907

18701908
_isValidScrollDate(date, throwWarning = true) {
1871-
const viewOffset = this.option('viewOffset') as number;
18721909
const min = this.getStartViewDate();
18731910
const max = this.getEndViewDate();
1911+
const { crossesMidnight } = this._getAdjustedHourBoundaries();
1912+
1913+
const dateDayStart = new Date(date.getFullYear(), date.getMonth(), date.getDate());
1914+
const minDayStart = new Date(min.getFullYear(), min.getMonth(), min.getDate());
1915+
const maxDayStart = new Date(max.getFullYear(), max.getMonth(), max.getDate());
1916+
1917+
let isDayValid = false;
1918+
if (crossesMidnight) {
1919+
// Boundaries cross midnight: allow minDay or nextDay
1920+
const nextDay = new Date(minDayStart);
1921+
nextDay.setDate(nextDay.getDate() + 1);
1922+
isDayValid = dateDayStart.getTime() === minDayStart.getTime()
1923+
|| dateDayStart.getTime() === nextDay.getTime()
1924+
|| (dateDayStart >= minDayStart && dateDayStart <= maxDayStart);
1925+
} else {
1926+
// Normal case: date should be within [minDay, maxDay]
1927+
isDayValid = dateDayStart >= minDayStart && dateDayStart <= maxDayStart;
1928+
}
18741929

1875-
const extendedMin = new Date(min.getTime() - viewOffset);
1876-
const extendedMax = new Date(max.getTime() + viewOffset);
1877-
1878-
if (date < extendedMin || date > extendedMax) {
1930+
if (!isDayValid) {
18791931
throwWarning && errors.log('W1008', date);
18801932
return false;
18811933
}

0 commit comments

Comments
 (0)