Skip to content

Commit 3ad15b1

Browse files
Scheduler: add new scrollTo api method (#32190)
1 parent 68899e6 commit 3ad15b1

9 files changed

Lines changed: 243 additions & 16 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
describe, expect, it, jest,
3+
} from '@jest/globals';
4+
import { logger } from '@ts/core/utils/m_console';
5+
6+
import { createScheduler } from './__mock__/create_scheduler';
7+
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';
8+
9+
describe('Scheduler scrollTo deprecation', () => {
10+
it('should log deprecation warning when using old scrollTo API', async () => {
11+
setupSchedulerTestEnvironment();
12+
const loggerWarnSpy = jest.spyOn(logger, 'warn');
13+
14+
const { scheduler } = await createScheduler({
15+
dataSource: [{
16+
text: 'Meeting',
17+
startDate: new Date(2025, 0, 15, 9, 0),
18+
endDate: new Date(2025, 0, 15, 10, 0),
19+
}],
20+
views: ['week'],
21+
currentView: 'week',
22+
currentDate: new Date(2025, 0, 15),
23+
startDayHour: 8,
24+
endDayHour: 18,
25+
});
26+
loggerWarnSpy.mockReset();
27+
28+
const testDate = new Date(2025, 0, 16, 14, 0);
29+
30+
scheduler.scrollTo(testDate, undefined, false);
31+
32+
expect(loggerWarnSpy).toHaveBeenCalledTimes(1);
33+
expect(loggerWarnSpy).toHaveBeenCalledWith(
34+
expect.stringContaining('W0002'),
35+
);
36+
});
37+
});

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

Lines changed: 128 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
2-
describe, expect, it, jest,
2+
beforeAll, beforeEach, describe, expect, it, jest,
33
} from '@jest/globals';
4+
import { logger } from '@ts/core/utils/m_console';
45

56
import { getResourceManagerMock } from '../__mock__/resource_manager.mock';
67
import SchedulerTimelineDay from '../workspaces/m_timeline_day';
@@ -12,13 +13,38 @@ import SchedulerWorkSpaceDay from '../workspaces/m_work_space_day';
1213
import SchedulerWorkSpaceMonth from '../workspaces/m_work_space_month';
1314
import SchedulerWorkSpaceWeek from '../workspaces/m_work_space_week';
1415
import SchedulerWorkSpaceWorkWeek from '../workspaces/m_work_space_work_week';
16+
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';
17+
18+
jest.mock('@ts/core/m_devices', () => {
19+
const originalModule: any = jest.requireActual('@ts/core/m_devices');
20+
const real = jest.fn().mockReturnValue({
21+
platform: 'mac',
22+
mac: true,
23+
deviceType: 'desktop',
24+
});
25+
const current = jest.fn().mockReturnValue({
26+
platform: 'mac',
27+
mac: true,
28+
deviceType: 'desktop',
29+
});
30+
31+
return {
32+
__esModule: true,
33+
default: {
34+
...originalModule.default,
35+
isSimulator: originalModule.default.isSimulator,
36+
real,
37+
current,
38+
},
39+
};
40+
});
1541

1642
type WorkspaceConstructor<T> = new (container: Element, options?: any) => T;
1743

1844
const createWorkspace = <T extends SchedulerWorkSpace>(
1945
WorkSpace: WorkspaceConstructor<T>,
2046
currentView: string,
21-
): T => {
47+
): { workspace: T; container: Element } => {
2248
const container = document.createElement('div');
2349
const workspace = new WorkSpace(container, {
2450
views: [currentView],
@@ -30,8 +56,9 @@ const createWorkspace = <T extends SchedulerWorkSpace>(
3056
(workspace as any)._isVisible = () => true;
3157
expect(container.classList).toContain('dx-scheduler-work-space');
3258

33-
return workspace;
59+
return { workspace, container };
3460
};
61+
3562
const workSpaces: {
3663
currentView: string;
3764
WorkSpace: WorkspaceConstructor<SchedulerWorkSpace>;
@@ -46,10 +73,14 @@ const workSpaces: {
4673
{ currentView: 'timelineMonth', WorkSpace: SchedulerTimelineMonth },
4774
];
4875

76+
beforeAll(() => {
77+
setupSchedulerTestEnvironment();
78+
});
79+
4980
describe('scheduler workspace', () => {
5081
workSpaces.forEach(({ currentView, WorkSpace }) => {
5182
it(`should clear cache on dimension change, view: ${currentView}`, () => {
52-
const workspace = createWorkspace(WorkSpace, currentView);
83+
const { workspace } = createWorkspace(WorkSpace, currentView);
5384
jest.spyOn(workspace.cache, 'clear');
5485

5586
workspace.cache.memo('test', () => 'value');
@@ -59,7 +90,7 @@ describe('scheduler workspace', () => {
5990
});
6091

6192
it(`should clear cache on _cleanView call, view: ${currentView}`, () => {
62-
const workspace = createWorkspace(WorkSpace, currentView);
93+
const { workspace } = createWorkspace(WorkSpace, currentView);
6394
jest.spyOn(workspace.cache, 'clear');
6495

6596
workspace.cache.memo('test', () => 'value');
@@ -70,3 +101,95 @@ describe('scheduler workspace', () => {
70101
});
71102
});
72103
});
104+
105+
describe('scheduler workspace scrollTo', () => {
106+
beforeEach(() => {
107+
setupSchedulerTestEnvironment();
108+
});
109+
110+
it('should change scroll position with center alignment', () => {
111+
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
112+
113+
const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
114+
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;
115+
116+
workspace.scrollTo(new Date(2017, 4, 25, 22, 0));
117+
118+
expect(scrollableContainer.scrollLeft).toBeCloseTo(11125);
119+
});
120+
121+
it('should not change scroll position when date is outside view range', () => {
122+
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
123+
124+
const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
125+
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;
126+
127+
workspace.scrollTo(new Date(2030, 0, 1));
128+
129+
expect(scrollableContainer.scrollLeft).toBeCloseTo(0);
130+
expect(scrollableContainer.scrollTop).toBeCloseTo(0);
131+
});
132+
133+
it('should scroll with start alignment', () => {
134+
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
135+
136+
const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
137+
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;
138+
139+
workspace.scrollTo(new Date(2017, 4, 25, 22, 0), undefined, false, true, 'start');
140+
141+
expect(scrollableContainer.scrollLeft).toBeCloseTo(11000);
142+
expect(scrollableContainer.scrollTop).toBeCloseTo(0);
143+
});
144+
145+
it('should scroll with center alignment', () => {
146+
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
147+
148+
const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
149+
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;
150+
151+
workspace.scrollTo(new Date(2017, 4, 25, 22, 0), undefined, false, true, 'center');
152+
153+
expect(scrollableContainer.scrollLeft).toBeCloseTo(11125);
154+
});
155+
156+
it('should scroll to all day panel when allDay is true', () => {
157+
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
158+
159+
const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
160+
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;
161+
162+
workspace.scrollTo(new Date(2017, 4, 25, 22, 0), undefined, true);
163+
164+
expect(scrollableContainer.scrollLeft).toBeCloseTo(11125);
165+
});
166+
167+
it('should handle throwWarning parameter correctly', () => {
168+
const loggerWarnSpy = jest.spyOn(logger, 'warn');
169+
loggerWarnSpy.mockReset();
170+
171+
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
172+
173+
const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
174+
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;
175+
176+
workspace.scrollTo(new Date(2030, 0, 1), undefined, false, true);
177+
178+
expect(scrollableContainer.scrollLeft).toBe(0);
179+
expect(scrollableContainer.scrollTop).toBe(0);
180+
expect(loggerWarnSpy).toHaveBeenCalledTimes(1);
181+
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('W1008'));
182+
});
183+
184+
it('should apply RTL offset when rtlEnabled is true', () => {
185+
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
186+
workspace.option('rtlEnabled', true);
187+
188+
const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
189+
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;
190+
191+
workspace.scrollTo(new Date(2017, 4, 25, 22, 0));
192+
193+
expect(scrollableContainer.scrollLeft).toBeCloseTo(-11125);
194+
});
195+
});

packages/devextreme/js/__internal/scheduler/m_scheduler.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ import { MobileTooltipStrategy } from './tooltip_strategies/m_mobile_tooltip_str
6464
import type {
6565
AppointmentTooltipItem,
6666
SafeAppointment,
67-
TargetedAppointment,
67+
ScrollToGroupValuesOrOptions, ScrollToOptions, TargetedAppointment,
6868
} from './types';
6969
import { AppointmentAdapter } from './utils/appointment_adapter/appointment_adapter';
7070
import { AppointmentDataAccessor } from './utils/data_accessor/appointment_data_accessor';
@@ -2022,8 +2022,31 @@ class Scheduler extends SchedulerOptionsBaseWidget {
20222022
this._appointmentTooltip?.hide();
20232023
}
20242024

2025-
scrollTo(date, groupValues, allDay) {
2026-
this._workSpace.scrollTo(date, groupValues, allDay);
2025+
scrollTo(
2026+
date: Date,
2027+
groupValuesOrOptions?: ScrollToGroupValuesOrOptions,
2028+
allDay?: boolean | undefined,
2029+
) {
2030+
let groupValues;
2031+
let allDayValue;
2032+
let align: 'start' | 'center' = 'center';
2033+
2034+
if (this._isScrollOptionsObject(groupValuesOrOptions)) {
2035+
groupValues = groupValuesOrOptions.group;
2036+
allDayValue = groupValuesOrOptions.allDay;
2037+
align = groupValuesOrOptions.alignInView ?? 'center';
2038+
} else {
2039+
errors.log('W0002', 'dxScheduler', 'scrollTo', '26.1', 'Use an object with "group", "allDay" and "alignInView" properties instead of separate parameters.');
2040+
groupValues = groupValuesOrOptions;
2041+
allDayValue = allDay;
2042+
}
2043+
2044+
this._workSpace.scrollTo(date, groupValues, allDayValue, true, align);
2045+
}
2046+
2047+
private _isScrollOptionsObject(options?: ScrollToGroupValuesOrOptions): options is ScrollToOptions {
2048+
return Boolean(options) && typeof options === 'object'
2049+
&& ('align' in options || 'allDay' in options || 'group' in options);
20272050
}
20282051

20292052
_isHorizontalVirtualScrolling() {

packages/devextreme/js/__internal/scheduler/types.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { dxElementWrapper } from '@js/core/renderer';
22
import type { Appointment, Properties } from '@js/ui/scheduler';
33

44
import type { ResourceLoader } from './utils/loader/resource_loader';
5+
import type { GroupValues, RawGroupValues } from './utils/resource_manager/types';
56
import type { AppointmentViewModelPlain } from './view_model/types';
67

78
export type Direction = 'vertical' | 'horizontal';
@@ -269,3 +270,14 @@ export interface CompactAppointmentOptions {
269270
allowDrag: boolean;
270271
isCompact: boolean;
271272
}
273+
274+
export interface ScrollToOptions {
275+
group?: RawGroupValues | GroupValues;
276+
allDay?: boolean | undefined;
277+
alignInView?: 'start' | 'center';
278+
}
279+
280+
export type ScrollToGroupValuesOrOptions = RawGroupValues
281+
| GroupValues
282+
| ScrollToOptions
283+
| undefined;

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,7 +1792,7 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {
17921792
return result;
17931793
}
17941794

1795-
scrollTo(date, groupValues?: RawGroupValues | GroupValues, allDay = false, throwWarning = true) {
1795+
scrollTo(date: Date, groupValues?: RawGroupValues | GroupValues, allDay = false, throwWarning = true, align: 'start' | 'center' = 'center') {
17961796
if (!this._isValidScrollDate(date, throwWarning)) {
17971797
return;
17981798
}
@@ -1819,8 +1819,8 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {
18191819
const scrollableWidth = getWidth($scrollable);
18201820
const cellHeight = this.getCellHeight();
18211821

1822-
const xShift = (scrollableWidth - cellWidth) / 2;
1823-
const yShift = (scrollableHeight - cellHeight) / 2;
1822+
const xShift = align === 'start' ? 0 : (scrollableWidth - cellWidth) / 2;
1823+
const yShift = align === 'start' ? 0 : (scrollableHeight - cellHeight) / 2;
18241824

18251825
const left = coordinates.left - scrollable.scrollLeft() - xShift - offset;
18261826
let top = coordinates.top - scrollable.scrollTop() - yShift;

packages/devextreme/js/ui/scheduler.d.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ export type AppointmentFormProperties = FormProperties & {
9696
/** @public */
9797
export type ViewType = 'agenda' | 'day' | 'month' | 'timelineDay' | 'timelineMonth' | 'timelineWeek' | 'timelineWorkWeek' | 'week' | 'workWeek';
9898
/** @public */
99+
export type SchedulerScrollToAlign = 'start' | 'center';
100+
/** @public */
99101
export type SchedulerPredefinedToolbarItem = 'today' | 'dateNavigator' | 'viewSwitcher';
100102
/** @public */
101103
export type SchedulerPredefinedDateNavigatorItem = 'prev' | 'next' | 'dateInterval';
@@ -1308,6 +1310,20 @@ export default class dxScheduler extends Widget<dxSchedulerOptions> {
13081310
* @public
13091311
*/
13101312
scrollTo(date: Date, group?: object, allDay?: boolean): void;
1313+
/**
1314+
* @docid
1315+
* @publicName scrollTo(date, options)
1316+
* @param2 options:Object|undefined
1317+
* @param2_field group:Object|undefined
1318+
* @param2_field allDay:Boolean|undefined
1319+
* @param2_field alignInView:Enums.SchedulerScrollToAlign|undefined
1320+
* @public
1321+
*/
1322+
scrollTo(date: Date, options?: {
1323+
group?: object;
1324+
allDay?: boolean;
1325+
alignInView?: SchedulerScrollToAlign;
1326+
}): void;
13111327
/**
13121328
* @docid
13131329
* @publicName showAppointmentPopup(appointmentData, createNewAppointment, currentAppointmentData)

packages/devextreme/js/ui/scheduler_types.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export {
88
AppointmentFormIconsShowMode,
99
AppointmentFormProperties,
1010
ViewType,
11+
SchedulerScrollToAlign,
1112
SchedulerPredefinedToolbarItem,
1213
SchedulerPredefinedDateNavigatorItem,
1314
AppointmentAddedEvent,

packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/scrollTo.tests.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,16 @@ module('ScrollTo', {
106106
scheduler.instance.scrollTo(new Date(2020, 8, 5));
107107
await waitAsync(0);
108108

109-
assert.equal(errors.log.callCount, 1, 'warning has been called once');
110-
assert.equal(errors.log.getCall(0).args[0], 'W1008', 'warning has correct error id');
109+
assert.equal(errors.log.callCount, 2, 'warnings have been called twice');
110+
assert.equal(errors.log.getCall(0).args[0], 'W0002', 'first warning is deprecation warning');
111+
assert.equal(errors.log.getCall(1).args[0], 'W1008', 'second warning has correct error id');
111112

112113
scheduler.instance.scrollTo(new Date(2020, 8, 14));
113114
await waitAsync(0);
114115

115-
assert.equal(errors.log.callCount, 2, 'warning has been called once');
116-
assert.equal(errors.log.getCall(1).args[0], 'W1008', 'warning has correct error id');
116+
assert.equal(errors.log.callCount, 4, 'warnings have been called four times total');
117+
assert.equal(errors.log.getCall(2).args[0], 'W0002', 'third warning is deprecation warning');
118+
assert.equal(errors.log.getCall(3).args[0], 'W1008', 'fourth warning has correct error id');
117119
});
118120

119121
test(`A warning should not be thrown when scrolling to a valid date when ${scrolling.text} is used`, async function(assert) {
@@ -122,7 +124,8 @@ module('ScrollTo', {
122124
scheduler.instance.scrollTo(new Date(2020, 8, 7));
123125
await waitAsync(0);
124126

125-
assert.equal(errors.log.callCount, 0, 'warning has been called once');
127+
assert.equal(errors.log.callCount, 1, 'deprecation warning has been called once');
128+
assert.equal(errors.log.getCall(0).args[0], 'W0002', 'warning is deprecation warning for old API');
126129
});
127130

128131
[{

packages/devextreme/ts/dx.all.d.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25877,6 +25877,17 @@ declare module DevExpress.ui {
2587725877
* [descr:dxScheduler.scrollTo(date, group, allDay)]
2587825878
*/
2587925879
scrollTo(date: Date, group?: object, allDay?: boolean): void;
25880+
/**
25881+
* [descr:dxScheduler.scrollTo(date, options)]
25882+
*/
25883+
scrollTo(
25884+
date: Date,
25885+
options?: {
25886+
group?: object;
25887+
allDay?: boolean;
25888+
alignInView?: DevExpress.ui.dxScheduler.SchedulerScrollToAlign;
25889+
}
25890+
): void;
2588025891
/**
2588125892
* [descr:dxScheduler.showAppointmentPopup(appointmentData, createNewAppointment, currentAppointmentData)]
2588225893
*/
@@ -26239,6 +26250,7 @@ declare module DevExpress.ui {
2623926250
| 'today'
2624026251
| 'dateNavigator'
2624126252
| 'viewSwitcher';
26253+
export type SchedulerScrollToAlign = 'start' | 'center';
2624226254
/**
2624326255
* [descr:TargetedAppointmentInfo]
2624426256
* @deprecated Attention! This type is for internal purposes only. If you used it previously, please submit a ticket to our {@link https://supportcenter.devexpress.com/ticket/create Support Center}. We will check if there is an alternative solution.

0 commit comments

Comments
 (0)