Skip to content

Commit 3c1eada

Browse files
authored
Scheduler: Provide the SnapToCellsMode option (#33021)
1 parent 1f714d8 commit 3c1eada

File tree

9 files changed

+358
-109
lines changed

9 files changed

+358
-109
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import type { Meta, StoryObj } from "@storybook/react-webpack5";
2+
import dxScheduler from "devextreme/ui/scheduler";
3+
import { wrapDxWithReact } from "../utils";
4+
import { resources } from "./data";
5+
6+
const Scheduler = wrapDxWithReact(dxScheduler);
7+
8+
const data = [
9+
{
10+
text: '1 minute',
11+
roomId: 1,
12+
assigneeId: [1],
13+
priorityId: 1,
14+
startDate: new Date(2026, 2, 15, 10, 0),
15+
endDate: new Date(2026, 2, 15, 10, 1)
16+
},
17+
{
18+
text: '5 minutes',
19+
roomId: 1,
20+
assigneeId: [2],
21+
priorityId: 1,
22+
startDate: new Date(2026, 2, 16, 10, 0),
23+
endDate: new Date(2026, 2, 16, 10, 5)
24+
},
25+
{
26+
text: '15 minutes',
27+
roomId: 2,
28+
assigneeId: [3],
29+
priorityId: 2,
30+
startDate: new Date(2026, 2, 17, 10, 0),
31+
endDate: new Date(2026, 2, 17, 10, 15)
32+
},
33+
{
34+
text: '30 minutes',
35+
roomId: 2,
36+
assigneeId: [1],
37+
priorityId: 2,
38+
startDate: new Date(2026, 2, 18, 10, 0),
39+
endDate: new Date(2026, 2, 18, 10, 30)
40+
},
41+
{
42+
text: '46 minutes',
43+
roomId: 3,
44+
assigneeId: [2],
45+
priorityId: 1,
46+
startDate: new Date(2026, 2, 19, 10, 0),
47+
endDate: new Date(2026, 2, 19, 10, 46)
48+
},
49+
{
50+
text: '1 hour',
51+
roomId: 2,
52+
assigneeId: [4],
53+
priorityId: 1,
54+
startDate: new Date(2026, 2, 20, 10, 0),
55+
endDate: new Date(2026, 2, 20, 11, 0)
56+
},
57+
];
58+
59+
const viewNames = ['day', 'week', 'workWeek', 'month', 'agenda', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth'];
60+
61+
const meta: Meta<typeof Scheduler> = {
62+
title: 'Components/Scheduler/SnapToCellsMode',
63+
component: Scheduler,
64+
parameters: { layout: 'padded' },
65+
};
66+
export default meta;
67+
68+
type Story = StoryObj<typeof Scheduler>;
69+
70+
export const Overview: Story = {
71+
args: {
72+
height: 600,
73+
views: viewNames,
74+
currentView: 'week',
75+
currentDate: new Date(2026, 2, 15),
76+
startDayHour: 9,
77+
endDayHour: 22,
78+
dataSource: data,
79+
resources,
80+
snapToCellsMode: 'auto',
81+
},
82+
argTypes: {
83+
height: { control: 'number' },
84+
views: { control: 'object' },
85+
snapToCellsMode: { control: 'select', options: ['always', 'auto', 'never'] },
86+
currentView: { control: 'select', options: viewNames },
87+
},
88+
};
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import {
2+
afterEach, beforeEach, describe, expect, it,
3+
} from '@jest/globals';
4+
5+
import { createScheduler } from './__mock__/create_scheduler';
6+
import {
7+
DEFAULT_CELL_HEIGHT,
8+
setupSchedulerTestEnvironment,
9+
} from './__mock__/m_mock_scheduler';
10+
11+
describe('snapToCellsMode', () => {
12+
beforeEach(() => {
13+
setupSchedulerTestEnvironment();
14+
});
15+
16+
afterEach(() => {
17+
document.body.innerHTML = '';
18+
});
19+
20+
it('default snapToCellsMode on day view', async () => {
21+
const { POM } = await createScheduler({
22+
width: 800,
23+
height: 600,
24+
views: ['day'],
25+
currentView: 'day',
26+
currentDate: new Date(2026, 2, 15),
27+
cellDuration: 30,
28+
startDayHour: 9,
29+
endDayHour: 18,
30+
dataSource: [{
31+
text: 'short',
32+
startDate: new Date(2026, 2, 15, 10, 0),
33+
endDate: new Date(2026, 2, 15, 10, 10),
34+
}],
35+
});
36+
37+
const appH = POM.getAppointment('short').getGeometry().height;
38+
39+
expect(appH).toBeLessThan(DEFAULT_CELL_HEIGHT / 2);
40+
});
41+
42+
it('root snapToCellsMode always overrides default on day view', async () => {
43+
const { POM } = await createScheduler({
44+
width: 800,
45+
height: 600,
46+
views: ['day'],
47+
currentView: 'day',
48+
currentDate: new Date(2026, 2, 15),
49+
cellDuration: 30,
50+
startDayHour: 9,
51+
endDayHour: 18,
52+
dataSource: [{
53+
text: 'short',
54+
startDate: new Date(2026, 2, 15, 10, 0),
55+
endDate: new Date(2026, 2, 15, 10, 10),
56+
}],
57+
snapToCellsMode: 'always',
58+
});
59+
60+
const appH = POM.getAppointment('short').getGeometry().height;
61+
62+
expect(appH).toEqual(DEFAULT_CELL_HEIGHT);
63+
});
64+
65+
it('views[].snapToCellsMode always overrides default on day view', async () => {
66+
const { POM } = await createScheduler({
67+
width: 800,
68+
height: 600,
69+
views: [{ type: 'day', snapToCellsMode: 'always' }],
70+
currentView: 'day',
71+
currentDate: new Date(2026, 2, 15),
72+
cellDuration: 30,
73+
startDayHour: 9,
74+
endDayHour: 18,
75+
dataSource: [{
76+
text: 'short',
77+
startDate: new Date(2026, 2, 15, 10, 0),
78+
endDate: new Date(2026, 2, 15, 10, 10),
79+
}],
80+
});
81+
82+
const appH = POM.getAppointment('short').getGeometry().height;
83+
84+
expect(appH).toEqual(DEFAULT_CELL_HEIGHT);
85+
});
86+
});

packages/devextreme/js/__internal/scheduler/utils/options/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = {
8888
mode: 'standard',
8989
},
9090
allDayPanelMode: 'all',
91+
snapToCellsMode: undefined,
9192
toolbar: {
9293
disabled: false,
9394
multiline: false,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ type RequiredOptions = 'views'
7777
| 'adaptivityEnabled'
7878
| 'scrolling'
7979
| 'allDayPanelMode'
80+
| 'snapToCellsMode'
8081
| 'toolbar';
8182
export type DateOption = 'currentDate' | 'min' | 'max';
8283
export type SafeSchedulerOptions = SchedulerInternalOptions

packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const getSchedulerMock = ({
1111
resourceManager,
1212
dateRange,
1313
skippedDays,
14+
isVirtualScrolling = false,
1415
}: {
1516
type: string;
1617
startDayHour: number;
@@ -19,6 +20,7 @@ export const getSchedulerMock = ({
1920
resourceManager?: ResourceManager;
2021
skippedDays?: number[];
2122
dateRange?: Date[];
23+
isVirtualScrolling?: boolean;
2224
}): Scheduler => ({
2325
timeZoneCalculator: mockTimeZoneCalculator,
2426
currentView: { type, skippedDays: skippedDays ?? [] },
@@ -37,6 +39,7 @@ export const getSchedulerMock = ({
3739
}[name]),
3840
option: (name: string) => ({ firstDayOfWeek: 0, showAllDayPanel: true }[name]),
3941
getViewOffsetMs: () => offsetMinutes * 60_000,
42+
isVirtualScrolling: () => isVirtualScrolling,
4043
resourceManager: resourceManager ?? new ResourceManager([]),
4144
_dataAccessors: mockAppointmentDataAccessor,
4245
}) as unknown as Scheduler;

packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export const sortAppointments = (
2323
const {
2424
isMonthView,
2525
hasAllDayPanel,
26+
snapToCellsMode,
2627
viewOffset,
2728
compareOptions: { endDayHour },
2829
} = optionManager.options;
@@ -40,9 +41,11 @@ export const sortAppointments = (
4041
sortByStartDate(innerStep1);
4142
sortByGroupIndex(innerStep1);
4243
const innerStep2 = addPosition(innerStep1, optionManager.getCells(panelName));
43-
const innerStep3 = isMonthView || panelName === 'allDayPanel'
44-
? snapToCells(innerStep2, optionManager.getCells(panelName))
45-
: innerStep2;
44+
const innerStep3 = snapToCells(
45+
innerStep2,
46+
optionManager.getCells(panelName),
47+
panelName === 'allDayPanel' ? 'always' : snapToCellsMode,
48+
);
4649
const innerStep4 = addCollector(innerStep3, optionManager.getCollectorOptions(panelName));
4750
return innerStep4;
4851
});

packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,48 @@
11
import type { Orientation } from '@js/common';
2+
import type { SnapToCellsMode } from '@js/ui/scheduler';
23
import type Scheduler from '@ts/scheduler/m_scheduler';
34

45
import type { ViewType } from '../../../types';
56
import { getCompareOptions } from '../../common/get_compare_options';
67
import type { CompareOptions } from '../../types';
78

8-
const configByView: Record<Exclude<ViewType, 'agenda'>, {
9+
interface ViewConfig {
910
isTimelineView: boolean;
1011
isMonthView: boolean;
1112
viewOrientation: 'horizontal' | 'vertical';
12-
}> = {
13-
day: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
14-
week: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
15-
workWeek: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
16-
month: { isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal' },
17-
timelineDay: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
18-
timelineWeek: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
19-
timelineWorkWeek: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
20-
timelineMonth: { isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal' },
13+
snapToCellsMode: SnapToCellsMode;
14+
}
15+
16+
const configByView: Record<Exclude<ViewType, 'agenda'>, ViewConfig> = {
17+
day: {
18+
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
19+
},
20+
week: {
21+
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
22+
},
23+
workWeek: {
24+
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
25+
},
26+
month: {
27+
isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always',
28+
},
29+
timelineDay: {
30+
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
31+
},
32+
timelineWeek: {
33+
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
34+
},
35+
timelineWorkWeek: {
36+
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
37+
},
38+
timelineMonth: {
39+
isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always',
40+
},
2141
};
2242

2343
export interface ViewModelOptions {
2444
type: ViewType;
45+
snapToCellsMode: SnapToCellsMode;
2546
viewOffset: number;
2647
groupOrientation?: Orientation;
2748
isGroupByDate: boolean;
@@ -47,16 +68,23 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions
4768
&& schedulerStore.getViewOption('groupByDate'),
4869
);
4970
const compareOptions = getCompareOptions(schedulerStore);
50-
const { isTimelineView, isMonthView, viewOrientation } = configByView[type];
71+
const {
72+
isTimelineView,
73+
isMonthView,
74+
viewOrientation,
75+
snapToCellsMode: defaultSnapToCellsMode,
76+
} = configByView[type];
5177
const isRTLEnabled = Boolean(schedulerStore.option('rtlEnabled'));
5278
const isAdaptivityEnabled = Boolean(schedulerStore.option('adaptivityEnabled'));
5379
const cellDurationMinutes = schedulerStore.getViewOption('cellDuration');
5480
const allDayPanelMode = schedulerStore.getViewOption('allDayPanelMode');
81+
const snapToCellsMode = schedulerStore.getViewOption('snapToCellsMode');
5582
const showAllDayPanel = schedulerStore.getViewOption('showAllDayPanel');
5683
const isVirtualScrolling = schedulerStore.isVirtualScrolling();
5784

5885
return {
5986
type,
87+
snapToCellsMode: snapToCellsMode ?? defaultSnapToCellsMode,
6088
viewOffset,
6189
groupOrientation,
6290
isGroupByDate,

0 commit comments

Comments
 (0)