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,88 @@
import type { Meta, StoryObj } from "@storybook/react-webpack5";
import dxScheduler from "devextreme/ui/scheduler";
import { wrapDxWithReact } from "../utils";
import { resources } from "./data";

const Scheduler = wrapDxWithReact(dxScheduler);

const data = [
{
text: '1 minute',
roomId: 1,
assigneeId: [1],
priorityId: 1,
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 1)
},
{
text: '5 minutes',
roomId: 1,
assigneeId: [2],
priorityId: 1,
startDate: new Date(2026, 2, 16, 10, 0),
endDate: new Date(2026, 2, 16, 10, 5)
},
{
text: '15 minutes',
roomId: 2,
assigneeId: [3],
priorityId: 2,
startDate: new Date(2026, 2, 17, 10, 0),
endDate: new Date(2026, 2, 17, 10, 15)
},
{
text: '30 minutes',
roomId: 2,
assigneeId: [1],
priorityId: 2,
startDate: new Date(2026, 2, 18, 10, 0),
endDate: new Date(2026, 2, 18, 10, 30)
},
{
text: '46 minutes',
roomId: 3,
assigneeId: [2],
priorityId: 1,
startDate: new Date(2026, 2, 19, 10, 0),
endDate: new Date(2026, 2, 19, 10, 46)
},
{
text: '1 hour',
roomId: 2,
assigneeId: [4],
priorityId: 1,
startDate: new Date(2026, 2, 20, 10, 0),
endDate: new Date(2026, 2, 20, 11, 0)
},
];

const viewNames = ['day', 'week', 'workWeek', 'month', 'agenda', 'timelineDay', 'timelineWeek', 'timelineWorkWeek', 'timelineMonth'];

const meta: Meta<typeof Scheduler> = {
title: 'Components/Scheduler/SnapToCellsMode',
component: Scheduler,
parameters: { layout: 'padded' },
};
export default meta;

type Story = StoryObj<typeof Scheduler>;

export const Overview: Story = {
args: {
height: 600,
views: viewNames,
currentView: 'week',
currentDate: new Date(2026, 2, 15),
startDayHour: 9,
endDayHour: 22,
dataSource: data,
resources,
snapToCellsMode: 'auto',
},
argTypes: {
height: { control: 'number' },
views: { control: 'object' },
snapToCellsMode: { control: 'select', options: ['always', 'auto', 'never'] },
currentView: { control: 'select', options: viewNames },
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
afterEach, beforeEach, describe, expect, it,
} from '@jest/globals';

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

describe('snapToCellsMode', () => {
beforeEach(() => {
setupSchedulerTestEnvironment();
});

afterEach(() => {
document.body.innerHTML = '';
});

it('default snapToCellsMode on day view', async () => {
const { POM } = await createScheduler({
width: 800,
height: 600,
views: ['day'],
currentView: 'day',
currentDate: new Date(2026, 2, 15),
cellDuration: 30,
startDayHour: 9,
endDayHour: 18,
dataSource: [{
text: 'short',
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 10),
}],
});

const appH = POM.getAppointment('short').getGeometry().height;

expect(appH).toBeLessThan(DEFAULT_CELL_HEIGHT * 0.45);
});

it('root snapToCellsMode always overrides default on day view', async () => {
const { POM } = await createScheduler({
width: 800,
height: 600,
views: ['day'],
currentView: 'day',
currentDate: new Date(2026, 2, 15),
cellDuration: 30,
startDayHour: 9,
endDayHour: 18,
dataSource: [{
text: 'short',
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 10),
}],
snapToCellsMode: 'always',
});

const appH = POM.getAppointment('short').getGeometry().height;

expect(appH).toBeGreaterThan(DEFAULT_CELL_HEIGHT * 0.85);
});

it('views[].snapToCellsMode always overrides default on day view', async () => {
const { POM } = await createScheduler({
width: 800,
height: 600,
views: [{ type: 'day', snapToCellsMode: 'always' }],
currentView: 'day',
currentDate: new Date(2026, 2, 15),
cellDuration: 30,
startDayHour: 9,
endDayHour: 18,
dataSource: [{
text: 'short',
startDate: new Date(2026, 2, 15, 10, 0),
endDate: new Date(2026, 2, 15, 10, 10),
}],
});

const appH = POM.getAppointment('short').getGeometry().height;

expect(appH).toBeGreaterThan(DEFAULT_CELL_HEIGHT * 0.85);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = {
mode: 'standard',
},
allDayPanelMode: 'all',
snapToCellsMode: undefined,
toolbar: {
disabled: false,
multiline: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ type RequiredOptions = 'views'
| 'adaptivityEnabled'
| 'scrolling'
| 'allDayPanelMode'
| 'snapToCellsMode'
| 'toolbar';
export type DateOption = 'currentDate' | 'min' | 'max';
export type SafeSchedulerOptions = SchedulerInternalOptions
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const getSchedulerMock = ({
resourceManager,
dateRange,
skippedDays,
isVirtualScrolling = false,
}: {
type: string;
startDayHour: number;
Expand All @@ -19,6 +20,7 @@ export const getSchedulerMock = ({
resourceManager?: ResourceManager;
skippedDays?: number[];
dateRange?: Date[];
isVirtualScrolling?: boolean;
}): Scheduler => ({
timeZoneCalculator: mockTimeZoneCalculator,
currentView: { type, skippedDays: skippedDays ?? [] },
Expand All @@ -37,6 +39,7 @@ export const getSchedulerMock = ({
}[name]),
option: (name: string) => ({ firstDayOfWeek: 0, showAllDayPanel: true }[name]),
getViewOffsetMs: () => offsetMinutes * 60_000,
isVirtualScrolling: () => isVirtualScrolling,
resourceManager: resourceManager ?? new ResourceManager([]),
_dataAccessors: mockAppointmentDataAccessor,
}) as unknown as Scheduler;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const sortAppointments = (
const {
isMonthView,
hasAllDayPanel,
snapToCellsMode,
viewOffset,
compareOptions: { endDayHour },
} = optionManager.options;
Expand All @@ -40,9 +41,11 @@ export const sortAppointments = (
sortByStartDate(innerStep1);
sortByGroupIndex(innerStep1);
const innerStep2 = addPosition(innerStep1, optionManager.getCells(panelName));
const innerStep3 = isMonthView || panelName === 'allDayPanel'
? snapToCells(innerStep2, optionManager.getCells(panelName))
: innerStep2;
const innerStep3 = snapToCells(
innerStep2,
optionManager.getCells(panelName),
panelName === 'allDayPanel' ? 'always' : snapToCellsMode,
);
Copy link
Contributor

@Tucchhaa Tucchhaa Mar 26, 2026

Choose a reason for hiding this comment

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

Can we add tests that check that scheduler.snapToCellsMode and scheduler.view.snapToCellsMode options actually have effect?

I mean if I have this config:

$().dxScheduler({
   snapToCellsMode: 'always',
   currentView: 'day'
})

or this config:

$().dxScheduler({
   view: [{ type: 'day', snapToCells: 'always' }]
   currentView: 'day'
})

then check that the option is applied

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added integration test which tests 3 cases for day view

  1. Checks default behaviour without setting snapToCellsMode
  2. Checks behaviour when root snapToCellsMode is set
  3. Checks behaviour when snapToCellsMode is set via views config

const innerStep4 = addCollector(innerStep3, optionManager.getCollectorOptions(panelName));
return innerStep4;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,48 @@
import type { Orientation } from '@js/common';
import type { SnapToCellsMode } from '@js/ui/scheduler';
import type Scheduler from '@ts/scheduler/m_scheduler';

import type { ViewType } from '../../../types';
import { getCompareOptions } from '../../common/get_compare_options';
import type { CompareOptions } from '../../types';

const configByView: Record<Exclude<ViewType, 'agenda'>, {
interface ViewConfig {
isTimelineView: boolean;
isMonthView: boolean;
viewOrientation: 'horizontal' | 'vertical';
}> = {
day: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
week: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
workWeek: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical' },
month: { isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal' },
timelineDay: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
timelineWeek: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
timelineWorkWeek: { isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal' },
timelineMonth: { isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal' },
snapToCellsMode: SnapToCellsMode;
}

const configByView: Record<Exclude<ViewType, 'agenda'>, ViewConfig> = {
day: {
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
},
week: {
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
},
workWeek: {
isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never',
},
month: {
isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always',
},
timelineDay: {
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
},
timelineWeek: {
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
},
timelineWorkWeek: {
isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never',
},
timelineMonth: {
isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always',
},
};

export interface ViewModelOptions {
type: ViewType;
snapToCellsMode: SnapToCellsMode;
viewOffset: number;
groupOrientation?: Orientation;
isGroupByDate: boolean;
Expand All @@ -47,16 +68,23 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions
&& schedulerStore.getViewOption('groupByDate'),
);
const compareOptions = getCompareOptions(schedulerStore);
const { isTimelineView, isMonthView, viewOrientation } = configByView[type];
const {
isTimelineView,
isMonthView,
viewOrientation,
snapToCellsMode: defaultSnapToCellsMode,
} = configByView[type];
const isRTLEnabled = Boolean(schedulerStore.option('rtlEnabled'));
const isAdaptivityEnabled = Boolean(schedulerStore.option('adaptivityEnabled'));
const cellDurationMinutes = schedulerStore.getViewOption('cellDuration');
const allDayPanelMode = schedulerStore.getViewOption('allDayPanelMode');
const snapToCellsMode = schedulerStore.getViewOption('snapToCellsMode');
const showAllDayPanel = schedulerStore.getViewOption('showAllDayPanel');
const isVirtualScrolling = schedulerStore.isVirtualScrolling();

return {
type,
snapToCellsMode: snapToCellsMode ?? defaultSnapToCellsMode,
viewOffset,
groupOrientation,
isGroupByDate,
Expand Down
Loading
Loading