From d51dca8347a15c1be14c7869e83ce0eaed0ee91c Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 23 Mar 2026 13:35:03 +0100 Subject: [PATCH 01/12] feat: add snapToCellsMode type and default fallback --- packages/devextreme/js/__internal/scheduler/types.ts | 1 + .../js/__internal/scheduler/utils/options/constants.ts | 1 + .../js/__internal/scheduler/utils/options/types.ts | 1 + .../options/get_view_model_options.ts | 9 ++++++++- 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/types.ts b/packages/devextreme/js/__internal/scheduler/types.ts index 25f85f1d6836..a8ca60b9e7b6 100644 --- a/packages/devextreme/js/__internal/scheduler/types.ts +++ b/packages/devextreme/js/__internal/scheduler/types.ts @@ -9,6 +9,7 @@ export type Direction = 'vertical' | 'horizontal'; export type GroupOrientation = 'vertical' | 'horizontal'; export type ViewType = 'agenda' | 'day' | 'month' | 'timelineDay' | 'timelineMonth' | 'timelineWeek' | 'timelineWorkWeek' | 'week' | 'workWeek'; export type AllDayPanelModeType = 'all' | 'allDay' | 'hidden'; +export type SnapToCellsModeType = 'always' | 'auto' | 'never'; export type RenderStrategyName = 'horizontal' | 'horizontalMonth' | 'horizontalMonthLine' | 'vertical' | 'week' | 'agenda'; export type FilterItemType = Record | string | number; export type HeaderCellTextFormat = string | ((date: Date) => string); diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts index b1fbdaab59ec..8bbab436d6c8 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants.ts @@ -88,6 +88,7 @@ export const DEFAULT_SCHEDULER_OPTIONS: Properties = { mode: 'standard', }, allDayPanelMode: 'all', + snapToCellsMode: undefined, toolbar: { disabled: false, multiline: false, diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts index f74a3670a0d3..b3e17098bce7 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/types.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/types.ts @@ -77,6 +77,7 @@ type RequiredOptions = 'views' | 'adaptivityEnabled' | 'scrolling' | 'allDayPanelMode' + | 'snapToCellsMode' | 'toolbar'; export type DateOption = 'currentDate' | 'min' | 'max'; export type SafeSchedulerOptions = SchedulerInternalOptions diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts index c5656557dcd0..5263776d5d0c 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts @@ -1,7 +1,7 @@ import type { Orientation } from '@js/common'; import type Scheduler from '@ts/scheduler/m_scheduler'; -import type { ViewType } from '../../../types'; +import type { SnapToCellsModeType, ViewType } from '../../../types'; import { getCompareOptions } from '../../common/get_compare_options'; import type { CompareOptions } from '../../types'; @@ -22,6 +22,7 @@ const configByView: Record, { export interface ViewModelOptions { type: ViewType; + snapToCellsMode: SnapToCellsModeType; viewOffset: number; groupOrientation?: Orientation; isGroupByDate: boolean; @@ -37,6 +38,10 @@ export interface ViewModelOptions { isVirtualScrolling: boolean; } +const getDefaultSnapToCellsModeForView = (type: ViewType): SnapToCellsModeType => ( + ['month', 'agenda', 'timelineMonth'].includes(type) ? 'always' : 'never' +); + export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions => { const viewOffset = schedulerStore.getViewOffsetMs(); const { groupOrientation, type } = schedulerStore.currentView; @@ -52,11 +57,13 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions 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 ?? getDefaultSnapToCellsModeForView(type), viewOffset, groupOrientation, isGroupByDate, From a1fc5184be8e66a6301f5ecf780b42764183f221 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 09:50:43 +0100 Subject: [PATCH 02/12] feat: implement snapToCellsMode logic --- .../generate_grid_view_model.ts | 9 ++- .../options/get_view_model_options.ts | 55 +++++++++++++------ .../steps/snap_to_cells.ts | 38 ++++++++++--- 3 files changed, 74 insertions(+), 28 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts index 8a64148ad289..2680e49cb311 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts @@ -23,6 +23,7 @@ export const sortAppointments = ( const { isMonthView, hasAllDayPanel, + snapToCellsMode, viewOffset, compareOptions: { endDayHour }, } = optionManager.options; @@ -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, + ); const innerStep4 = addCollector(innerStep3, optionManager.getCollectorOptions(panelName)); return innerStep4; }); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts index 5263776d5d0c..5a9bf9b8c358 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts @@ -5,19 +5,41 @@ import type { SnapToCellsModeType, ViewType } from '../../../types'; import { getCompareOptions } from '../../common/get_compare_options'; import type { CompareOptions } from '../../types'; -const configByView: Record, { +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: SnapToCellsModeType; +} + +const configByView: Record = { + 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', + }, + agenda: { + isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'always', + }, }; export interface ViewModelOptions { @@ -38,10 +60,6 @@ export interface ViewModelOptions { isVirtualScrolling: boolean; } -const getDefaultSnapToCellsModeForView = (type: ViewType): SnapToCellsModeType => ( - ['month', 'agenda', 'timelineMonth'].includes(type) ? 'always' : 'never' -); - export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions => { const viewOffset = schedulerStore.getViewOffsetMs(); const { groupOrientation, type } = schedulerStore.currentView; @@ -52,7 +70,12 @@ 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'); @@ -63,7 +86,7 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions return { type, - snapToCellsMode: snapToCellsMode ?? getDefaultSnapToCellsModeForView(type), + snapToCellsMode: snapToCellsMode ?? defaultSnapToCellsMode, viewOffset, groupOrientation, isGroupByDate, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts index 7d0d02b85e4b..43ab9c6e7eac 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts @@ -1,24 +1,44 @@ +import type { SnapToCellsModeType } from '../../../types'; import type { CellInterval, ListEntity, Position, } from '../../types'; +const getCellFill = ( + startDateUTC: number, + endDateUTC: number, + cell: CellInterval, +): number => { + const cellDuration = cell.max - cell.min; + if (cellDuration <= 0) return 0; + + const overlapStart = Math.max(startDateUTC, cell.min); + const overlapEnd = Math.min(endDateUTC, cell.max); + const overlapDuration = Math.max(0, overlapEnd - overlapStart); + + return overlapDuration / cellDuration; +}; + export const snapToCells = ( entities: T[], cells: CellInterval[], - isSnapToCell = true, + mode: SnapToCellsModeType = 'always', ): T[] => { - if (!isSnapToCell) { - return entities; - } + if (mode === 'never') return entities; return entities.map((entity) => { - const { cellIndex, endCellIndex } = entity; + const startCell = cells[entity.cellIndex]; + const endCell = cells[entity.endCellIndex]; + + const snapStart = mode === 'always' + || getCellFill(entity.startDateUTC, entity.endDateUTC, startCell) > 0.5; + const snapEnd = mode === 'always' + || getCellFill(entity.startDateUTC, entity.endDateUTC, endCell) > 0.5; + + const startDateUTC = snapStart ? startCell.min : entity.startDateUTC; + const endDateUTC = snapEnd ? endCell.max : entity.endDateUTC; return { - ...entity, - startDateUTC: cells[cellIndex].min, - endDateUTC: cells[endCellIndex].max, - duration: cells[endCellIndex].max - cells[cellIndex].min, + ...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC, }; }); }; From d37d2b3ccb58b61feb0e046f0180c437e2514d46 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 09:51:27 +0100 Subject: [PATCH 03/12] tests: add tests --- .../view_model/__mock__/scheduler.mock.ts | 3 + .../options/get_view_model_options.test.ts | 25 +++ .../steps/snap_to_cells.test.ts | 189 ++++++++++-------- .../steps/snap_to_cells.ts | 5 +- 4 files changed, 134 insertions(+), 88 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts b/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts index 729bace90338..98483bff3457 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/__mock__/scheduler.mock.ts @@ -11,6 +11,7 @@ export const getSchedulerMock = ({ resourceManager, dateRange, skippedDays, + isVirtualScrolling = false, }: { type: string; startDayHour: number; @@ -19,6 +20,7 @@ export const getSchedulerMock = ({ resourceManager?: ResourceManager; skippedDays?: number[]; dateRange?: Date[]; + isVirtualScrolling?: boolean; }): Scheduler => ({ timeZoneCalculator: mockTimeZoneCalculator, currentView: { type, skippedDays: skippedDays ?? [] }, @@ -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; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts new file mode 100644 index 000000000000..438d8e8f59c3 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from '@jest/globals'; +import { getSchedulerMock } from '@ts/scheduler/view_model/__mock__/scheduler.mock'; + +import { getViewModelOptions } from './get_view_model_options'; + +describe('getViewModelOptions', () => { + it.each([ + { viewType: 'month' as const, expected: 'always' }, + { viewType: 'agenda' as const, expected: 'always' }, + { viewType: 'timelineMonth' as const, expected: 'always' }, + { viewType: 'day' as const, expected: 'never' }, + { viewType: 'week' as const, expected: 'never' }, + { viewType: 'workWeek' as const, expected: 'never' }, + { viewType: 'timelineDay' as const, expected: 'never' }, + { viewType: 'timelineWeek' as const, expected: 'never' }, + { viewType: 'timelineWorkWeek' as const, expected: 'never' }, + ])('should use $expected snapToCellsMode by default for $viewType', ({ viewType, expected }) => { + expect(getViewModelOptions(getSchedulerMock({ + type: viewType, + startDayHour: 0, + endDayHour: 24, + offsetMinutes: 0, + })).snapToCellsMode).toBe(expected); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.test.ts index d64e547ba71f..b8294dcbb5f1 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.test.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.test.ts @@ -2,94 +2,109 @@ import { describe, expect, it } from '@jest/globals'; import { snapToCells } from './snap_to_cells'; +const cells = [ + { + min: 0, max: 10, cellIndex: 0, rowIndex: 0, columnIndex: 0, + }, + { + min: 10, max: 20, cellIndex: 1, rowIndex: 0, columnIndex: 1, + }, + { + min: 20, max: 30, cellIndex: 2, rowIndex: 0, columnIndex: 2, + }, + { + min: 30, max: 40, cellIndex: 3, rowIndex: 1, columnIndex: 0, + }, + { + min: 40, max: 50, cellIndex: 4, rowIndex: 2, columnIndex: 1, + }, +]; + describe('snapToCells', () => { - it('should snap appointments to cells', () => { - const items = [{ - duration: 0, - cellIndex: 0, - endCellIndex: 0, - rowIndex: 0, - columnIndex: 0, - }, - { - duration: 0, - cellIndex: 3, - endCellIndex: 4, - rowIndex: 1, - columnIndex: 0, - }, - { - duration: 0, - cellIndex: 0, - endCellIndex: 2, - rowIndex: 0, - columnIndex: 0, - }, - { - duration: 0, - cellIndex: 4, - endCellIndex: 4, - rowIndex: 2, - columnIndex: 1, - }, - { - duration: 0, - cellIndex: 5, - endCellIndex: 5, - rowIndex: 3, - columnIndex: 2, - }]; + describe('always mode', () => { + it('should snap appointments to cell boundaries', () => { + const items = [ + { + cellIndex: 0, endCellIndex: 0, startDateUTC: 2, endDateUTC: 8, duration: 6, + }, + { + cellIndex: 3, endCellIndex: 4, startDateUTC: 32, endDateUTC: 48, duration: 16, + }, + { + cellIndex: 0, endCellIndex: 2, startDateUTC: 3, endDateUTC: 27, duration: 24, + }, + ]; + + expect(snapToCells(items as any, cells, 'always')).toEqual([ + expect.objectContaining({ + startDateUTC: 0, endDateUTC: 10, duration: 10, + }), + expect.objectContaining({ + startDateUTC: 30, endDateUTC: 50, duration: 20, + }), + expect.objectContaining({ + startDateUTC: 0, endDateUTC: 30, duration: 30, + }), + ]); + }); + }); + + describe('never mode', () => { + it('should return same reference without changes', () => { + const items = [ + { + cellIndex: 0, endCellIndex: 0, startDateUTC: 2, endDateUTC: 10, duration: 8, + }, + { + cellIndex: 1, endCellIndex: 2, startDateUTC: 12, endDateUTC: 27, duration: 15, + }, + ]; + + expect(snapToCells(items as any, cells, 'never')).toBe(items); + }); + }); + + describe('auto mode', () => { + it('should snap both boundaries when cells are covered by more than 50%', () => { + const items = [ + { + cellIndex: 0, endCellIndex: 1, startDateUTC: 2, endDateUTC: 16, duration: 14, + }, + ]; + + expect(snapToCells(items as any, cells, 'auto')).toEqual([ + expect.objectContaining({ + startDateUTC: 0, endDateUTC: 20, duration: 20, + }), + ]); + }); + + it('should not snap boundary when cell is covered by less than 50%', () => { + const items = [ + { + cellIndex: 0, endCellIndex: 0, startDateUTC: 2, endDateUTC: 7, duration: 5, + }, + ]; + + expect(snapToCells(items as any, cells, 'auto')).toEqual([ + expect.objectContaining({ + startDateUTC: 2, endDateUTC: 7, duration: 5, + }), + ]); + }); + + it('should not snap boundary when cell is covered by exactly 50%', () => { + const items = [ + { + cellIndex: 0, endCellIndex: 0, startDateUTC: 0, endDateUTC: 5, duration: 5, + }, + ]; - expect(snapToCells(items as any, [ - { - min: 0, max: 10, cellIndex: 0, rowIndex: 0, columnIndex: 0, - }, - { - min: 10, max: 20, cellIndex: 1, rowIndex: 0, columnIndex: 1, - }, - { - min: 20, max: 30, cellIndex: 2, rowIndex: 0, columnIndex: 2, - }, - { - min: 30, max: 40, cellIndex: 3, rowIndex: 1, columnIndex: 0, - }, - { - min: 40, max: 50, cellIndex: 4, rowIndex: 2, columnIndex: 1, - }, - { - min: 50, max: 60, cellIndex: 5, rowIndex: 3, columnIndex: 2, - }, - ])).toEqual([ - { - ...items[0], - duration: 10, - startDateUTC: 0, - endDateUTC: 10, - }, - { - ...items[1], - duration: 20, - startDateUTC: 30, - endDateUTC: 50, - }, - { - ...items[2], - duration: 30, - startDateUTC: 0, - endDateUTC: 30, - }, - { - ...items[3], - duration: 10, - startDateUTC: 40, - endDateUTC: 50, - }, - { - ...items[4], - duration: 10, - startDateUTC: 50, - endDateUTC: 60, - }, - ]); + expect(snapToCells(items as any, cells, 'auto')).toEqual([ + expect.objectContaining({ + startDateUTC: 0, endDateUTC: 5, duration: 5, + }), + ]); + }); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts index 43ab9c6e7eac..d0ddf0ae1603 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts @@ -38,7 +38,10 @@ export const snapToCells = ( const endDateUTC = snapEnd ? endCell.max : entity.endDateUTC; return { - ...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC, + ...entity, + startDateUTC, + endDateUTC, + duration: endDateUTC - startDateUTC, }; }); }; From 7263bf76592e4c5e5ea84cf26ced7070ee729490 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 11:42:39 +0100 Subject: [PATCH 04/12] feat: add storybook --- .../SchedulerSnapToCellsMode.stories.tsx | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx diff --git a/apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx b/apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx new file mode 100644 index 000000000000..59d74040117e --- /dev/null +++ b/apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx @@ -0,0 +1,108 @@ +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 = { + title: 'Components/Scheduler/SnapToCellsMode', + component: Scheduler, + parameters: { layout: 'padded' }, +}; +export default meta; + +type Story = StoryObj; + +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 }, + }, +}; + +export const PerViewOverrides: Story = { + args: { + height: 600, + views: viewNames, + currentView: 'timelineMonth', + currentDate: new Date(2026, 2, 15), + startDayHour: 9, + endDayHour: 22, + dataSource: data, + resources, + snapToCellsMode: 'never', + }, + argTypes: { + height: { control: 'number' }, + views: { control: 'object' }, + snapToCellsMode: { control: 'select', options: ['always', 'auto', 'never'] }, + currentView: { control: 'select', options: viewNames }, + }, +}; From fc05556d8d7d61c66051493d3810712f179ab34d Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 25 Mar 2026 09:58:08 +0100 Subject: [PATCH 05/12] refactor: remove unused story --- .../SchedulerSnapToCellsMode.stories.tsx | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx b/apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx index 59d74040117e..e3450c180f40 100644 --- a/apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx +++ b/apps/react-storybook/stories/scheduler/SchedulerSnapToCellsMode.stories.tsx @@ -86,23 +86,3 @@ export const Overview: Story = { currentView: { control: 'select', options: viewNames }, }, }; - -export const PerViewOverrides: Story = { - args: { - height: 600, - views: viewNames, - currentView: 'timelineMonth', - currentDate: new Date(2026, 2, 15), - startDayHour: 9, - endDayHour: 22, - dataSource: data, - resources, - snapToCellsMode: 'never', - }, - argTypes: { - height: { control: 'number' }, - views: { control: 'object' }, - snapToCellsMode: { control: 'select', options: ['always', 'auto', 'never'] }, - currentView: { control: 'select', options: viewNames }, - }, -}; From 7980fd56725a8a21f33db5e79aac196c9efcf5e6 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Wed, 25 Mar 2026 14:00:37 +0100 Subject: [PATCH 06/12] test: remove test --- .../options/get_view_model_options.test.ts | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts deleted file mode 100644 index 438d8e8f59c3..000000000000 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { describe, expect, it } from '@jest/globals'; -import { getSchedulerMock } from '@ts/scheduler/view_model/__mock__/scheduler.mock'; - -import { getViewModelOptions } from './get_view_model_options'; - -describe('getViewModelOptions', () => { - it.each([ - { viewType: 'month' as const, expected: 'always' }, - { viewType: 'agenda' as const, expected: 'always' }, - { viewType: 'timelineMonth' as const, expected: 'always' }, - { viewType: 'day' as const, expected: 'never' }, - { viewType: 'week' as const, expected: 'never' }, - { viewType: 'workWeek' as const, expected: 'never' }, - { viewType: 'timelineDay' as const, expected: 'never' }, - { viewType: 'timelineWeek' as const, expected: 'never' }, - { viewType: 'timelineWorkWeek' as const, expected: 'never' }, - ])('should use $expected snapToCellsMode by default for $viewType', ({ viewType, expected }) => { - expect(getViewModelOptions(getSchedulerMock({ - type: viewType, - startDayHour: 0, - endDayHour: 24, - offsetMinutes: 0, - })).snapToCellsMode).toBe(expected); - }); -}); From d01b9f5aa44b3d057b4416afa975b078acb5d025 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 26 Mar 2026 10:42:36 +0100 Subject: [PATCH 07/12] refactor: replace SnapToCellsModeType with SnapToCellsMode in scheduler types --- packages/devextreme/js/__internal/scheduler/types.ts | 1 - .../generate_view_model/options/get_view_model_options.ts | 7 ++++--- .../view_model/generate_view_model/steps/snap_to_cells.ts | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/types.ts b/packages/devextreme/js/__internal/scheduler/types.ts index a8ca60b9e7b6..25f85f1d6836 100644 --- a/packages/devextreme/js/__internal/scheduler/types.ts +++ b/packages/devextreme/js/__internal/scheduler/types.ts @@ -9,7 +9,6 @@ export type Direction = 'vertical' | 'horizontal'; export type GroupOrientation = 'vertical' | 'horizontal'; export type ViewType = 'agenda' | 'day' | 'month' | 'timelineDay' | 'timelineMonth' | 'timelineWeek' | 'timelineWorkWeek' | 'week' | 'workWeek'; export type AllDayPanelModeType = 'all' | 'allDay' | 'hidden'; -export type SnapToCellsModeType = 'always' | 'auto' | 'never'; export type RenderStrategyName = 'horizontal' | 'horizontalMonth' | 'horizontalMonthLine' | 'vertical' | 'week' | 'agenda'; export type FilterItemType = Record | string | number; export type HeaderCellTextFormat = string | ((date: Date) => string); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts index 5a9bf9b8c358..8fde6349adfa 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts @@ -1,7 +1,8 @@ import type { Orientation } from '@js/common'; +import type { SnapToCellsMode } from '@js/ui/scheduler'; import type Scheduler from '@ts/scheduler/m_scheduler'; -import type { SnapToCellsModeType, ViewType } from '../../../types'; +import type { ViewType } from '../../../types'; import { getCompareOptions } from '../../common/get_compare_options'; import type { CompareOptions } from '../../types'; @@ -9,7 +10,7 @@ interface ViewConfig { isTimelineView: boolean; isMonthView: boolean; viewOrientation: 'horizontal' | 'vertical'; - snapToCellsMode: SnapToCellsModeType; + snapToCellsMode: SnapToCellsMode; } const configByView: Record = { @@ -44,7 +45,7 @@ const configByView: Record = { export interface ViewModelOptions { type: ViewType; - snapToCellsMode: SnapToCellsModeType; + snapToCellsMode: SnapToCellsMode; viewOffset: number; groupOrientation?: Orientation; isGroupByDate: boolean; diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts index d0ddf0ae1603..f8de32dfd18e 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts @@ -1,4 +1,5 @@ -import type { SnapToCellsModeType } from '../../../types'; +import type { SnapToCellsMode } from '@js/ui/scheduler'; + import type { CellInterval, ListEntity, Position, } from '../../types'; @@ -21,7 +22,7 @@ const getCellFill = ( export const snapToCells = ( entities: T[], cells: CellInterval[], - mode: SnapToCellsModeType = 'always', + mode: SnapToCellsMode = 'always', ): T[] => { if (mode === 'never') return entities; From dea354c2c68a4ddfaccfbe44bc3ed32f599462bd Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 26 Mar 2026 10:46:03 +0100 Subject: [PATCH 08/12] refactor: exclude 'agenda' view from configByView in scheduler options --- .../generate_view_model/options/get_view_model_options.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts index 8fde6349adfa..5154129aeee5 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts @@ -13,7 +13,7 @@ interface ViewConfig { snapToCellsMode: SnapToCellsMode; } -const configByView: Record = { +const configByView: Record, ViewConfig> = { day: { isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never', }, @@ -38,9 +38,6 @@ const configByView: Record = { timelineMonth: { isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always', }, - agenda: { - isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'always', - }, }; export interface ViewModelOptions { From 05bef987f43b5840b84e2db175714c47a2c684d3 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 26 Mar 2026 11:32:38 +0100 Subject: [PATCH 09/12] test: add integration tests for snapToCellsMode in scheduler --- .../integration.snapToCellsMode.tests.js | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js new file mode 100644 index 000000000000..3cb319c4322c --- /dev/null +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js @@ -0,0 +1,102 @@ +import 'fluent_blue_light.css!'; + +import fx from 'common/core/animation/fx'; +import { createWrapper, initTestMarkup } from '../../helpers/scheduler/helpers.js'; +import { waitAsync } from '../../helpers/scheduler/waitForAsync.js'; + +import '__internal/scheduler/m_scheduler'; + +QUnit.testStart(() => initTestMarkup()); + +const moduleConfig = { + beforeEach() { + fx.off = true; + }, + afterEach() { + fx.off = false; + }, +}; + +QUnit.module('Integration: snapToCellsMode', moduleConfig, () => { + QUnit.test('default snapToCellsMode on day view', async function(assert) { + const scheduler = await createWrapper({ + 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), + }], + }); + await waitAsync(0); + + const cellH = scheduler.workSpace.getCellHeight(); + const appH = scheduler.appointments.getAppointmentHeight(0); + + assert.ok( + appH < cellH * 0.45, + `default snapToCellsMode: height ${appH} should be clearly less than cell height ${cellH}`, + ); + }); + + QUnit.test('root snapToCellsMode overrides default snapToCellsMode on day view', async function(assert) { + const scheduler = await createWrapper({ + 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', + }); + await waitAsync(0); + + const cellH = scheduler.workSpace.getCellHeight(); + const appH = scheduler.appointments.getAppointmentHeight(0); + + assert.ok( + appH > cellH * 0.85, + `height ${appH} should be most of cell height ${cellH}`, + ); + }); + + QUnit.test('views[].snapToCellsMode overrides default snapToCellsMode on day view', async function(assert) { + const scheduler = await createWrapper({ + 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), + }], + }); + await waitAsync(0); + + const cellH = scheduler.workSpace.getCellHeight(); + const appH = scheduler.appointments.getAppointmentHeight(0); + + assert.ok( + appH > cellH * 0.85, + `views[].snapToCellsMode always: height ${appH} should be most of cell height ${cellH}`, + ); + }); +}); From 999944c10feed7c1b69f32e41de285bfcfb1fd95 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 26 Mar 2026 11:52:42 +0100 Subject: [PATCH 10/12] refactor: update snapToCellsMode handling in scheduler view options --- .../scheduler/utils/options/constants_view.ts | 20 +++++++++++-------- .../options/get_view_model_options.ts | 20 +++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts index d01f374237bd..8939ee6ab2c5 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts @@ -1,3 +1,5 @@ +import type { SnapToCellsMode } from '@js/ui/scheduler'; + import type { AgendaView, View, ViewType } from './types'; export const VIEWS: Record = { @@ -17,25 +19,27 @@ const WEEKENDS = [0, 6]; const getView = ( type: ViewType, groupOrientation: View['groupOrientation'], + snapToCellsMode: SnapToCellsMode, skippedDays: number[] = [], ): View => ({ groupOrientation, intervalCount: 1, type, skippedDays, + snapToCellsMode, }); export const DEFAULT_VIEW_OPTIONS: Record, View> & { agenda: AgendaView; } = { - day: getView('day', 'horizontal'), - week: getView('week', 'horizontal'), - workWeek: getView('workWeek', 'horizontal', WEEKENDS), - month: getView('month', 'horizontal'), - timelineDay: getView('timelineDay', 'vertical'), - timelineWeek: getView('timelineWeek', 'vertical'), - timelineWorkWeek: getView('timelineWorkWeek', 'vertical', WEEKENDS), - timelineMonth: getView('timelineMonth', 'vertical'), + day: getView('day', 'horizontal', 'never'), + week: getView('week', 'horizontal', 'never'), + workWeek: getView('workWeek', 'horizontal', 'never', WEEKENDS), + month: getView('month', 'horizontal', 'always'), + timelineDay: getView('timelineDay', 'vertical', 'never'), + timelineWeek: getView('timelineWeek', 'vertical', 'never'), + timelineWorkWeek: getView('timelineWorkWeek', 'vertical', 'never', WEEKENDS), + timelineMonth: getView('timelineMonth', 'vertical', 'always'), agenda: { agendaDuration: 7, intervalCount: 1, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts index 5154129aeee5..8700e33d4792 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts @@ -10,33 +10,32 @@ interface ViewConfig { isTimelineView: boolean; isMonthView: boolean; viewOrientation: 'horizontal' | 'vertical'; - snapToCellsMode: SnapToCellsMode; } const configByView: Record, ViewConfig> = { day: { - isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never', + isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', }, week: { - isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never', + isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', }, workWeek: { - isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never', + isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', }, month: { - isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always', + isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal', }, timelineDay: { - isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never', + isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', }, timelineWeek: { - isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never', + isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', }, timelineWorkWeek: { - isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never', + isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', }, timelineMonth: { - isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always', + isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', }, }; @@ -72,7 +71,6 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions isTimelineView, isMonthView, viewOrientation, - snapToCellsMode: defaultSnapToCellsMode, } = configByView[type]; const isRTLEnabled = Boolean(schedulerStore.option('rtlEnabled')); const isAdaptivityEnabled = Boolean(schedulerStore.option('adaptivityEnabled')); @@ -84,7 +82,7 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions return { type, - snapToCellsMode: snapToCellsMode ?? defaultSnapToCellsMode, + snapToCellsMode, viewOffset, groupOrientation, isGroupByDate, From 64d461b62b4d5b5fb81c07391106d393fffae57d Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 26 Mar 2026 12:22:43 +0100 Subject: [PATCH 11/12] Revert "refactor: update snapToCellsMode handling in scheduler view options" This reverts commit 999944c10feed7c1b69f32e41de285bfcfb1fd95. --- .../scheduler/utils/options/constants_view.ts | 20 ++++++++----------- .../options/get_view_model_options.ts | 20 ++++++++++--------- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts index 8939ee6ab2c5..d01f374237bd 100644 --- a/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts +++ b/packages/devextreme/js/__internal/scheduler/utils/options/constants_view.ts @@ -1,5 +1,3 @@ -import type { SnapToCellsMode } from '@js/ui/scheduler'; - import type { AgendaView, View, ViewType } from './types'; export const VIEWS: Record = { @@ -19,27 +17,25 @@ const WEEKENDS = [0, 6]; const getView = ( type: ViewType, groupOrientation: View['groupOrientation'], - snapToCellsMode: SnapToCellsMode, skippedDays: number[] = [], ): View => ({ groupOrientation, intervalCount: 1, type, skippedDays, - snapToCellsMode, }); export const DEFAULT_VIEW_OPTIONS: Record, View> & { agenda: AgendaView; } = { - day: getView('day', 'horizontal', 'never'), - week: getView('week', 'horizontal', 'never'), - workWeek: getView('workWeek', 'horizontal', 'never', WEEKENDS), - month: getView('month', 'horizontal', 'always'), - timelineDay: getView('timelineDay', 'vertical', 'never'), - timelineWeek: getView('timelineWeek', 'vertical', 'never'), - timelineWorkWeek: getView('timelineWorkWeek', 'vertical', 'never', WEEKENDS), - timelineMonth: getView('timelineMonth', 'vertical', 'always'), + day: getView('day', 'horizontal'), + week: getView('week', 'horizontal'), + workWeek: getView('workWeek', 'horizontal', WEEKENDS), + month: getView('month', 'horizontal'), + timelineDay: getView('timelineDay', 'vertical'), + timelineWeek: getView('timelineWeek', 'vertical'), + timelineWorkWeek: getView('timelineWorkWeek', 'vertical', WEEKENDS), + timelineMonth: getView('timelineMonth', 'vertical'), agenda: { agendaDuration: 7, intervalCount: 1, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts index 8700e33d4792..5154129aeee5 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts @@ -10,32 +10,33 @@ interface ViewConfig { isTimelineView: boolean; isMonthView: boolean; viewOrientation: 'horizontal' | 'vertical'; + snapToCellsMode: SnapToCellsMode; } const configByView: Record, ViewConfig> = { day: { - isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', + isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never', }, week: { - isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', + isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never', }, workWeek: { - isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', + isTimelineView: false, isMonthView: false, viewOrientation: 'vertical', snapToCellsMode: 'never', }, month: { - isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal', + isTimelineView: false, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always', }, timelineDay: { - isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', + isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never', }, timelineWeek: { - isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', + isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never', }, timelineWorkWeek: { - isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', + isTimelineView: true, isMonthView: false, viewOrientation: 'horizontal', snapToCellsMode: 'never', }, timelineMonth: { - isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', + isTimelineView: true, isMonthView: true, viewOrientation: 'horizontal', snapToCellsMode: 'always', }, }; @@ -71,6 +72,7 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions isTimelineView, isMonthView, viewOrientation, + snapToCellsMode: defaultSnapToCellsMode, } = configByView[type]; const isRTLEnabled = Boolean(schedulerStore.option('rtlEnabled')); const isAdaptivityEnabled = Boolean(schedulerStore.option('adaptivityEnabled')); @@ -82,7 +84,7 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions return { type, - snapToCellsMode, + snapToCellsMode: snapToCellsMode ?? defaultSnapToCellsMode, viewOffset, groupOrientation, isGroupByDate, From 6e4fccc0cf23c5ea17db6118c465c51fa7163234 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Thu, 26 Mar 2026 14:12:57 +0100 Subject: [PATCH 12/12] test: rewrite qunit test to jest --- .../__tests__/snap_to_cells_mode.test.ts | 86 +++++++++++++++ .../integration.snapToCellsMode.tests.js | 102 ------------------ 2 files changed, 86 insertions(+), 102 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/snap_to_cells_mode.test.ts delete mode 100644 packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/snap_to_cells_mode.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/snap_to_cells_mode.test.ts new file mode 100644 index 000000000000..2663d5e2c533 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/snap_to_cells_mode.test.ts @@ -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); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js deleted file mode 100644 index 3cb319c4322c..000000000000 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.snapToCellsMode.tests.js +++ /dev/null @@ -1,102 +0,0 @@ -import 'fluent_blue_light.css!'; - -import fx from 'common/core/animation/fx'; -import { createWrapper, initTestMarkup } from '../../helpers/scheduler/helpers.js'; -import { waitAsync } from '../../helpers/scheduler/waitForAsync.js'; - -import '__internal/scheduler/m_scheduler'; - -QUnit.testStart(() => initTestMarkup()); - -const moduleConfig = { - beforeEach() { - fx.off = true; - }, - afterEach() { - fx.off = false; - }, -}; - -QUnit.module('Integration: snapToCellsMode', moduleConfig, () => { - QUnit.test('default snapToCellsMode on day view', async function(assert) { - const scheduler = await createWrapper({ - 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), - }], - }); - await waitAsync(0); - - const cellH = scheduler.workSpace.getCellHeight(); - const appH = scheduler.appointments.getAppointmentHeight(0); - - assert.ok( - appH < cellH * 0.45, - `default snapToCellsMode: height ${appH} should be clearly less than cell height ${cellH}`, - ); - }); - - QUnit.test('root snapToCellsMode overrides default snapToCellsMode on day view', async function(assert) { - const scheduler = await createWrapper({ - 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', - }); - await waitAsync(0); - - const cellH = scheduler.workSpace.getCellHeight(); - const appH = scheduler.appointments.getAppointmentHeight(0); - - assert.ok( - appH > cellH * 0.85, - `height ${appH} should be most of cell height ${cellH}`, - ); - }); - - QUnit.test('views[].snapToCellsMode overrides default snapToCellsMode on day view', async function(assert) { - const scheduler = await createWrapper({ - 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), - }], - }); - await waitAsync(0); - - const cellH = scheduler.workSpace.getCellHeight(); - const appH = scheduler.appointments.getAppointmentHeight(0); - - assert.ok( - appH > cellH * 0.85, - `views[].snapToCellsMode always: height ${appH} should be most of cell height ${cellH}`, - ); - }); -});