From 7cf0fa3a6e590c09fbd7f1286a48f0d98bbd2db9 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 23 Mar 2026 13:29:41 +0100 Subject: [PATCH 01/10] css: hide appointment strip when container is narrow in fluent theme --- .../scss/widgets/fluent/scheduler/_index.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss index 1e86184428c2..e87f83628ef6 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss @@ -870,6 +870,8 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px; } .dx-scheduler-appointment:not(.dx-scheduler-appointment-has-resource-color) { + container-type: inline-size; + .dx-scheduler-appointment-strip { display: block; position: absolute; @@ -877,6 +879,12 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px; height: 100%; background-color: $base-accent; } + + @container (max-width: 8px) { + .dx-scheduler-appointment-strip { + display: none; + } + } } .dx-rtl.dx-scheduler-appointment:not(.dx-scheduler-appointment-has-resource-color) { From 6ca069a8fba5f3773fb2373149aec342aa81b426 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 23 Mar 2026 13:35:03 +0100 Subject: [PATCH 02/10] 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 280a304c235378ca031f2847b439e0027d59f7ac Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 23 Mar 2026 14:30:32 +0100 Subject: [PATCH 03/10] feat: implement snapToCellsMode logic --- .../generate_grid_view_model.ts | 9 +++-- .../steps/snap_to_cells.ts | 37 ++++++++++++++----- 2 files changed, 34 insertions(+), 12 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/steps/snap_to_cells.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/steps/snap_to_cells.ts index 7d0d02b85e4b..90cf05f4c4d4 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,43 @@ +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; + + const threshold = mode === 'always' ? 0 : 0.5; return entities.map((entity) => { - const { cellIndex, endCellIndex } = entity; + const startCell = cells[entity.cellIndex]; + const endCell = cells[entity.endCellIndex]; + + const startDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, startCell) > threshold + ? startCell.min : entity.startDateUTC; + const endDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, endCell) > threshold + ? 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 ddf3596a105b55c05eb12c6ac7e8392a54c734ec Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 23 Mar 2026 15:32:38 +0100 Subject: [PATCH 04/10] tests: add tests --- .../options/get_view_model_options.test.ts | 19 ++ .../options/get_view_model_options.ts | 2 +- .../steps/snap_to_cells.test.ts | 189 ++++++++++-------- 3 files changed, 122 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/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..7f7e2f75468c --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from '@jest/globals'; + +import { getDefaultSnapToCellsModeForView } from './get_view_model_options'; + +describe('getDefaultSnapToCellsModeForView', () => { + 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 return $expected for $viewType', ({ viewType, expected }) => { + expect(getDefaultSnapToCellsModeForView(viewType)).toBe(expected); + }); +}); 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..0916c94dd9a4 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 @@ -38,7 +38,7 @@ export interface ViewModelOptions { isVirtualScrolling: boolean; } -const getDefaultSnapToCellsModeForView = (type: ViewType): SnapToCellsModeType => ( +export const getDefaultSnapToCellsModeForView = (type: ViewType): SnapToCellsModeType => ( ['month', 'agenda', 'timelineMonth'].includes(type) ? 'always' : 'never' ); 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, + }), + ]); + }); }); }); From b4a2064f9cc6000755ee42b70a5c2ced6da82230 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Mon, 23 Mar 2026 16:57:36 +0100 Subject: [PATCH 05/10] fix: fix tests --- .../generate_view_model/steps/snap_to_cells.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) 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 90cf05f4c4d4..48484422aabe 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 @@ -25,15 +25,24 @@ export const snapToCells = ( ): T[] => { if (mode === 'never') return entities; - const threshold = mode === 'always' ? 0 : 0.5; + if (mode === 'always') { + return entities.map((entity) => { + const startDateUTC = cells[entity.cellIndex].min; + const endDateUTC = cells[entity.endCellIndex].max; + + return { + ...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC, + }; + }); + } return entities.map((entity) => { const startCell = cells[entity.cellIndex]; const endCell = cells[entity.endCellIndex]; - const startDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, startCell) > threshold + const startDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, startCell) > 0.5 ? startCell.min : entity.startDateUTC; - const endDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, endCell) > threshold + const endDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, endCell) > 0.5 ? endCell.max : entity.endDateUTC; return { From 3aa684a6f13bbd6acdc257e7b2fe9f4e77ba7694 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 09:11:51 +0100 Subject: [PATCH 06/10] refactor: optmize code --- .../steps/snap_to_cells.ts | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) 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 48484422aabe..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 @@ -25,25 +25,17 @@ export const snapToCells = ( ): T[] => { if (mode === 'never') return entities; - if (mode === 'always') { - return entities.map((entity) => { - const startDateUTC = cells[entity.cellIndex].min; - const endDateUTC = cells[entity.endCellIndex].max; - - return { - ...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC, - }; - }); - } - return entities.map((entity) => { const startCell = cells[entity.cellIndex]; const endCell = cells[entity.endCellIndex]; - const startDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, startCell) > 0.5 - ? startCell.min : entity.startDateUTC; - const endDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, endCell) > 0.5 - ? endCell.max : entity.endDateUTC; + 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, endDateUTC, duration: endDateUTC - startDateUTC, From 81d204ffba143f839fdd5607bf506e3fb03a2277 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 09:30:36 +0100 Subject: [PATCH 07/10] refactor: optimize get_view_model_options.ts --- .../view_model/__mock__/scheduler.mock.ts | 3 + .../options/get_view_model_options.test.ts | 14 +++-- .../options/get_view_model_options.ts | 55 +++++++++++++------ 3 files changed, 52 insertions(+), 20 deletions(-) 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 index 7f7e2f75468c..438d8e8f59c3 100644 --- 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 @@ -1,8 +1,9 @@ import { describe, expect, it } from '@jest/globals'; +import { getSchedulerMock } from '@ts/scheduler/view_model/__mock__/scheduler.mock'; -import { getDefaultSnapToCellsModeForView } from './get_view_model_options'; +import { getViewModelOptions } from './get_view_model_options'; -describe('getDefaultSnapToCellsModeForView', () => { +describe('getViewModelOptions', () => { it.each([ { viewType: 'month' as const, expected: 'always' }, { viewType: 'agenda' as const, expected: 'always' }, @@ -13,7 +14,12 @@ describe('getDefaultSnapToCellsModeForView', () => { { viewType: 'timelineDay' as const, expected: 'never' }, { viewType: 'timelineWeek' as const, expected: 'never' }, { viewType: 'timelineWorkWeek' as const, expected: 'never' }, - ])('should return $expected for $viewType', ({ viewType, expected }) => { - expect(getDefaultSnapToCellsModeForView(viewType)).toBe(expected); + ])('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/options/get_view_model_options.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/options/get_view_model_options.ts index 0916c94dd9a4..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; } -export 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, From a1cc484c97dd6e1ae200e40698f111275b75567c Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 10:23:10 +0100 Subject: [PATCH 08/10] Revert "refactor: optimize get_view_model_options.ts" This reverts commit 81d204ffba143f839fdd5607bf506e3fb03a2277. --- .../view_model/__mock__/scheduler.mock.ts | 3 - .../options/get_view_model_options.test.ts | 14 ++--- .../options/get_view_model_options.ts | 55 ++++++------------- 3 files changed, 20 insertions(+), 52 deletions(-) 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 98483bff3457..729bace90338 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,7 +11,6 @@ export const getSchedulerMock = ({ resourceManager, dateRange, skippedDays, - isVirtualScrolling = false, }: { type: string; startDayHour: number; @@ -20,7 +19,6 @@ export const getSchedulerMock = ({ resourceManager?: ResourceManager; skippedDays?: number[]; dateRange?: Date[]; - isVirtualScrolling?: boolean; }): Scheduler => ({ timeZoneCalculator: mockTimeZoneCalculator, currentView: { type, skippedDays: skippedDays ?? [] }, @@ -39,7 +37,6 @@ 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 index 438d8e8f59c3..7f7e2f75468c 100644 --- 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 @@ -1,9 +1,8 @@ import { describe, expect, it } from '@jest/globals'; -import { getSchedulerMock } from '@ts/scheduler/view_model/__mock__/scheduler.mock'; -import { getViewModelOptions } from './get_view_model_options'; +import { getDefaultSnapToCellsModeForView } from './get_view_model_options'; -describe('getViewModelOptions', () => { +describe('getDefaultSnapToCellsModeForView', () => { it.each([ { viewType: 'month' as const, expected: 'always' }, { viewType: 'agenda' as const, expected: 'always' }, @@ -14,12 +13,7 @@ describe('getViewModelOptions', () => { { 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); + ])('should return $expected for $viewType', ({ viewType, expected }) => { + expect(getDefaultSnapToCellsModeForView(viewType)).toBe(expected); }); }); 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..0916c94dd9a4 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,41 +5,19 @@ import type { SnapToCellsModeType, ViewType } from '../../../types'; import { getCompareOptions } from '../../common/get_compare_options'; import type { CompareOptions } from '../../types'; -interface ViewConfig { +const configByView: Record, { isTimelineView: boolean; isMonthView: boolean; viewOrientation: 'horizontal' | 'vertical'; - 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', - }, +}> = { + 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' }, }; export interface ViewModelOptions { @@ -60,6 +38,10 @@ export interface ViewModelOptions { isVirtualScrolling: boolean; } +export 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; @@ -70,12 +52,7 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions && schedulerStore.getViewOption('groupByDate'), ); const compareOptions = getCompareOptions(schedulerStore); - const { - isTimelineView, - isMonthView, - viewOrientation, - snapToCellsMode: defaultSnapToCellsMode, - } = configByView[type]; + const { isTimelineView, isMonthView, viewOrientation } = configByView[type]; const isRTLEnabled = Boolean(schedulerStore.option('rtlEnabled')); const isAdaptivityEnabled = Boolean(schedulerStore.option('adaptivityEnabled')); const cellDurationMinutes = schedulerStore.getViewOption('cellDuration'); @@ -86,7 +63,7 @@ export const getViewModelOptions = (schedulerStore: Scheduler): ViewModelOptions return { type, - snapToCellsMode: snapToCellsMode ?? defaultSnapToCellsMode, + snapToCellsMode: snapToCellsMode ?? getDefaultSnapToCellsModeForView(type), viewOffset, groupOrientation, isGroupByDate, From c40345773a0e2026f9dcfa2ed863382f0707b4ec Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 10:23:10 +0100 Subject: [PATCH 09/10] Revert "refactor: optmize code" This reverts commit 3aa684a6f13bbd6acdc257e7b2fe9f4e77ba7694. --- .../steps/snap_to_cells.ts | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) 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..48484422aabe 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 @@ -25,17 +25,25 @@ export const snapToCells = ( ): T[] => { if (mode === 'never') return entities; + if (mode === 'always') { + return entities.map((entity) => { + const startDateUTC = cells[entity.cellIndex].min; + const endDateUTC = cells[entity.endCellIndex].max; + + return { + ...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC, + }; + }); + } + return entities.map((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; + const startDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, startCell) > 0.5 + ? startCell.min : entity.startDateUTC; + const endDateUTC = getCellFill(entity.startDateUTC, entity.endDateUTC, endCell) > 0.5 + ? endCell.max : entity.endDateUTC; return { ...entity, startDateUTC, endDateUTC, duration: endDateUTC - startDateUTC, From 63ec56529c5f131e91ceb4c1eba73dc41dd57a70 Mon Sep 17 00:00:00 2001 From: Sergio Bur Date: Tue, 24 Mar 2026 10:42:45 +0100 Subject: [PATCH 10/10] css: fix bug --- .../devextreme-scss/scss/widgets/fluent/scheduler/_index.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss index e87f83628ef6..506ea19a44c5 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss @@ -870,7 +870,7 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px; } .dx-scheduler-appointment:not(.dx-scheduler-appointment-has-resource-color) { - container-type: inline-size; + container-type: size; .dx-scheduler-appointment-strip { display: block;