Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
describe, expect, it, jest,
} from '@jest/globals';
import { logger } from '@ts/core/utils/m_console';

import { createScheduler } from './__mock__/create_scheduler';
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';

describe('Scheduler scrollTo deprecation', () => {
it('should log deprecation warning when using old scrollTo API', async () => {
setupSchedulerTestEnvironment();
const loggerWarnSpy = jest.spyOn(logger, 'warn');

const { scheduler } = await createScheduler({
dataSource: [{
text: 'Meeting',
startDate: new Date(2025, 0, 15, 9, 0),
endDate: new Date(2025, 0, 15, 10, 0),
}],
views: ['week'],
currentView: 'week',
currentDate: new Date(2025, 0, 15),
startDayHour: 8,
endDayHour: 18,
});
loggerWarnSpy.mockReset();

const testDate = new Date(2025, 0, 16, 14, 0);

scheduler.scrollTo(testDate, undefined, false);

expect(loggerWarnSpy).toHaveBeenCalledTimes(1);
expect(loggerWarnSpy).toHaveBeenCalledWith(
expect.stringContaining('W0002'),
);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
describe, expect, it, jest,
beforeAll, beforeEach, describe, expect, it, jest,
} from '@jest/globals';
import { logger } from '@ts/core/utils/m_console';

import { getResourceManagerMock } from '../__mock__/resource_manager.mock';
import SchedulerTimelineDay from '../workspaces/m_timeline_day';
Expand All @@ -12,13 +13,38 @@ import SchedulerWorkSpaceDay from '../workspaces/m_work_space_day';
import SchedulerWorkSpaceMonth from '../workspaces/m_work_space_month';
import SchedulerWorkSpaceWeek from '../workspaces/m_work_space_week';
import SchedulerWorkSpaceWorkWeek from '../workspaces/m_work_space_work_week';
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';

jest.mock('@ts/core/m_devices', () => {
const originalModule: any = jest.requireActual('@ts/core/m_devices');
const real = jest.fn().mockReturnValue({
platform: 'mac',
mac: true,
deviceType: 'desktop',
});
const current = jest.fn().mockReturnValue({
platform: 'mac',
mac: true,
deviceType: 'desktop',
});

return {
__esModule: true,
default: {
...originalModule.default,
isSimulator: originalModule.default.isSimulator,
real,
current,
},
};
});

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

const createWorkspace = <T extends SchedulerWorkSpace>(
WorkSpace: WorkspaceConstructor<T>,
currentView: string,
): T => {
): { workspace: T; container: Element } => {
const container = document.createElement('div');
const workspace = new WorkSpace(container, {
views: [currentView],
Expand All @@ -30,8 +56,9 @@ const createWorkspace = <T extends SchedulerWorkSpace>(
(workspace as any)._isVisible = () => true;
expect(container.classList).toContain('dx-scheduler-work-space');

return workspace;
return { workspace, container };
};

const workSpaces: {
currentView: string;
WorkSpace: WorkspaceConstructor<SchedulerWorkSpace>;
Expand All @@ -46,10 +73,14 @@ const workSpaces: {
{ currentView: 'timelineMonth', WorkSpace: SchedulerTimelineMonth },
];

beforeAll(() => {
setupSchedulerTestEnvironment();
});

describe('scheduler workspace', () => {
workSpaces.forEach(({ currentView, WorkSpace }) => {
it(`should clear cache on dimension change, view: ${currentView}`, () => {
const workspace = createWorkspace(WorkSpace, currentView);
const { workspace } = createWorkspace(WorkSpace, currentView);
jest.spyOn(workspace.cache, 'clear');

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

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

workspace.cache.memo('test', () => 'value');
Expand All @@ -70,3 +101,95 @@ describe('scheduler workspace', () => {
});
});
});

describe('scheduler workspace scrollTo', () => {
beforeEach(() => {
setupSchedulerTestEnvironment();
});

it('should change scroll position with center alignment', () => {
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');

const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;

workspace.scrollTo(new Date(2017, 4, 25, 22, 0));

expect(scrollableContainer.scrollLeft).toBeCloseTo(11125);
});

it('should not change scroll position when date is outside view range', () => {
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');

const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;

workspace.scrollTo(new Date(2030, 0, 1));

expect(scrollableContainer.scrollLeft).toBeCloseTo(0);
expect(scrollableContainer.scrollTop).toBeCloseTo(0);
});

it('should scroll with start alignment', () => {
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');

const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;

workspace.scrollTo(new Date(2017, 4, 25, 22, 0), undefined, false, true, 'start');

expect(scrollableContainer.scrollLeft).toBeCloseTo(11000);
expect(scrollableContainer.scrollTop).toBeCloseTo(0);
});

it('should scroll with center alignment', () => {
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');

const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;

workspace.scrollTo(new Date(2017, 4, 25, 22, 0), undefined, false, true, 'center');

expect(scrollableContainer.scrollLeft).toBeCloseTo(11125);
});

it('should scroll to all day panel when allDay is true', () => {
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');

const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;

workspace.scrollTo(new Date(2017, 4, 25, 22, 0), undefined, true);

expect(scrollableContainer.scrollLeft).toBeCloseTo(11125);
});

it('should handle throwWarning parameter correctly', () => {
const loggerWarnSpy = jest.spyOn(logger, 'warn');
loggerWarnSpy.mockReset();

const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');

const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;

workspace.scrollTo(new Date(2030, 0, 1), undefined, false, true);

expect(scrollableContainer.scrollLeft).toBe(0);
expect(scrollableContainer.scrollTop).toBe(0);
expect(loggerWarnSpy).toHaveBeenCalledTimes(1);
expect(loggerWarnSpy).toHaveBeenCalledWith(expect.stringContaining('W1008'));
});

it('should apply RTL offset when rtlEnabled is true', () => {
const { workspace, container } = createWorkspace(SchedulerTimelineDay, 'timelineDay');
workspace.option('rtlEnabled', true);

const scrollableElement = container.querySelector('.dx-scheduler-date-table-scrollable') as HTMLElement;
const scrollableContainer = scrollableElement.querySelector('.dx-scrollable-container') as HTMLElement;

workspace.scrollTo(new Date(2017, 4, 25, 22, 0));

expect(scrollableContainer.scrollLeft).toBeCloseTo(-11125);
});
});
29 changes: 26 additions & 3 deletions packages/devextreme/js/__internal/scheduler/m_scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { MobileTooltipStrategy } from './tooltip_strategies/m_mobile_tooltip_str
import type {
AppointmentTooltipItem,
SafeAppointment,
TargetedAppointment,
ScrollToGroupValuesOrOptions, ScrollToOptions, TargetedAppointment,
} from './types';
import { AppointmentAdapter } from './utils/appointment_adapter/appointment_adapter';
import { AppointmentDataAccessor } from './utils/data_accessor/appointment_data_accessor';
Expand Down Expand Up @@ -2022,8 +2022,31 @@ class Scheduler extends SchedulerOptionsBaseWidget {
this._appointmentTooltip?.hide();
}

scrollTo(date, groupValues, allDay) {
this._workSpace.scrollTo(date, groupValues, allDay);
scrollTo(
date: Date,
groupValuesOrOptions?: ScrollToGroupValuesOrOptions,
allDay?: boolean | undefined,
) {
let groupValues;
let allDayValue;
let align: 'start' | 'center' = 'center';

if (this._isScrollOptionsObject(groupValuesOrOptions)) {
groupValues = groupValuesOrOptions.group;
allDayValue = groupValuesOrOptions.allDay;
align = groupValuesOrOptions.align ?? 'center';
} else {
errors.log('W0002', 'dxScheduler', 'scrollTo', '26.1', 'Use an object with "group", "allDay", and "align" properties instead of separate parameters.');
groupValues = groupValuesOrOptions;
allDayValue = allDay;
}

this._workSpace.scrollTo(date, groupValues, allDayValue, true, align);
}

private _isScrollOptionsObject(options?: ScrollToGroupValuesOrOptions): options is ScrollToOptions {
return Boolean(options) && typeof options === 'object'
&& ('align' in options || 'allDay' in options || 'group' in options);
Comment on lines +2048 to +2049
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

The type guard logic will incorrectly identify group values objects as the new options object if they happen to have an 'allDay' or 'group' property. Since group values can be arbitrary objects (e.g., { ownerId: 2 }), the type guard should only check for the presence of the 'align' property to definitively identify the new API usage. Update the condition to: return Boolean(options) && typeof options === 'object' && 'align' in options;

Suggested change
return Boolean(options) && typeof options === 'object'
&& ('align' in options || 'allDay' in options || 'group' in options);
return Boolean(options) && typeof options === 'object' && 'align' in options;

Copilot uses AI. Check for mistakes.
}

_isHorizontalVirtualScrolling() {
Expand Down
13 changes: 13 additions & 0 deletions packages/devextreme/js/__internal/scheduler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { dxElementWrapper } from '@js/core/renderer';
import type { Appointment, Properties } from '@js/ui/scheduler';

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

export type Direction = 'vertical' | 'horizontal';
Expand Down Expand Up @@ -269,3 +270,15 @@ export interface CompactAppointmentOptions {
allowDrag: boolean;
isCompact: boolean;
}

export interface ScrollToOptions {
group?: RawGroupValues | GroupValues;
allDay?: boolean | undefined;
align?: 'start' | 'center';
}

export type ScrollToGroupValuesOrOptions =
| RawGroupValues
| GroupValues
| ScrollToOptions
| undefined;
Original file line number Diff line number Diff line change
Expand Up @@ -1823,7 +1823,7 @@ class SchedulerWorkSpace extends Widget<WorkspaceOptionsInternal> {
return result;
}

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

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

const left = coordinates.left - scrollable.scrollLeft() - xShift - offset;
let top = coordinates.top - scrollable.scrollTop() - yShift;
Expand Down
14 changes: 14 additions & 0 deletions packages/devextreme/js/ui/scheduler.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1308,6 +1308,20 @@ export default class dxScheduler extends Widget<dxSchedulerOptions> {
* @public
*/
scrollTo(date: Date, group?: object, allDay?: boolean): void;
/**
* @docid
* @publicName scrollTo(date, options)
* @param2 options:Object|undefined
* @param2_field group:Object|undefined
* @param2_field allDay:Boolean|undefined
* @param2_field align:Enums.SchedulerScrollToAlign|undefined
* @public
*/
scrollTo(date: Date, options?: {
group?: object;
allDay?: boolean;
align?: 'start' | 'center';
}): void;
/**
* @docid
* @publicName showAppointmentPopup(appointmentData, createNewAppointment, currentAppointmentData)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,16 @@ module('ScrollTo', {
scheduler.instance.scrollTo(new Date(2020, 8, 5));
await waitAsync(0);

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

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

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

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

assert.equal(errors.log.callCount, 0, 'warning has been called once');
assert.equal(errors.log.callCount, 1, 'deprecation warning has been called once');
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

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

The existing QUnit tests have been updated to expect deprecation warnings for the old API, but there are no new tests in this file that verify the new options-based API works correctly with the align parameter. Consider adding tests that call scrollTo(date, { align: 'start' }) and scrollTo(date, { align: 'center', group: ..., allDay: ... }) to verify the new API functionality.

Copilot uses AI. Check for mistakes.
assert.equal(errors.log.getCall(0).args[0], 'W0002', 'warning is deprecation warning for old API');
});

[{
Expand Down
11 changes: 11 additions & 0 deletions packages/devextreme/ts/dx.all.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25849,6 +25849,17 @@ declare module DevExpress.ui {
* [descr:dxScheduler.scrollTo(date, group, allDay)]
*/
scrollTo(date: Date, group?: object, allDay?: boolean): void;
/**
* [descr:dxScheduler.scrollTo(date, options)]
*/
scrollTo(
date: Date,
options?: {
group?: object;
allDay?: boolean;
align?: 'start' | 'center';
}
): void;
/**
* [descr:dxScheduler.showAppointmentPopup(appointmentData, createNewAppointment, currentAppointmentData)]
*/
Expand Down
Loading