From ac067ec5e492d0a34ec1645877304f3d7f19c315 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 8 Aug 2026 15:42:49 -0300 Subject: [PATCH 1/2] Scheduler: fix all day appointment resizing in timeline and vertical views --- .../appointments_resizing_vertical.test.ts | 73 ++++++++++++++++ .../appointments/appointment/m_appointment.ts | 7 +- .../__internal/scheduler/m_subscribes.test.ts | 84 +++++++++++++++++++ .../js/__internal/scheduler/m_subscribes.ts | 11 ++- 4 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 packages/devextreme/js/__internal/scheduler/__tests__/appointments_resizing_vertical.test.ts create mode 100644 packages/devextreme/js/__internal/scheduler/m_subscribes.test.ts diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_resizing_vertical.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_resizing_vertical.test.ts new file mode 100644 index 000000000000..51bf2ae20e82 --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_resizing_vertical.test.ts @@ -0,0 +1,73 @@ +import { + beforeEach, describe, expect, it, +} from '@jest/globals'; +import type { Properties } from '@js/ui/scheduler'; + +import { createScheduler } from './__mock__/create_scheduler'; +import { setupSchedulerTestEnvironment } from './__mock__/mock_scheduler'; + +const APPOINTMENT_SELECTOR = '.dx-scheduler-appointment'; +const HANDLE_TOP_SELECTOR = '.dx-resizable-handle-top'; +const HANDLE_BOTTOM_SELECTOR = '.dx-resizable-handle-bottom'; + +const getResizeHandles = (container: HTMLElement): string[][] => Array + .from(container.querySelectorAll(APPOINTMENT_SELECTOR)) + .map((part) => [ + ...(part.querySelector(HANDLE_TOP_SELECTOR) ? ['top'] : []), + ...(part.querySelector(HANDLE_BOTTOM_SELECTOR) ? ['bottom'] : []), + ]); + +const baseConfig: Properties = { + currentDate: new Date(2021, 3, 12), + views: ['week'], + currentView: 'week', + editing: { allowUpdating: true, allowResizing: true }, + height: 600, +}; + +describe('Appointments resizing in vertical views', () => { + beforeEach(() => { + setupSchedulerTestEnvironment(); + }); + + it('should render both resize handles on an appointment that is not split', async () => { + const { container } = await createScheduler({ + ...baseConfig, + dataSource: [{ + text: 'Short', + startDate: new Date(2021, 3, 12, 9), + endDate: new Date(2021, 3, 12, 11), + }], + }); + + expect(getResizeHandles(container)).toEqual([['top', 'bottom']]); + }); + + it('should render resize handles only on the edges of an appointment split by midnight', async () => { + const { container } = await createScheduler({ + ...baseConfig, + dataSource: [{ + text: 'Long', + startDate: new Date(2021, 3, 12, 22), + endDate: new Date(2021, 3, 13, 3), + }], + }); + + expect(getResizeHandles(container)).toEqual([['top'], ['bottom']]); + }); + + it('should render resize handles only on the edges of an all day appointment when the all day panel is hidden', async () => { + const { container } = await createScheduler({ + ...baseConfig, + allDayPanelMode: 'hidden', + dataSource: [{ + text: 'All day', + allDay: true, + startDate: new Date(2021, 3, 12, 5), + endDate: new Date(2021, 3, 13, 5), + }], + }); + + expect(getResizeHandles(container)).toEqual([['top'], [], ['bottom']]); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts index 828f6172d282..878020afe5e6 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/m_appointment.ts @@ -138,10 +138,15 @@ export class Appointment extends DOMComponent { } _getVerticalResizingRule() { + const reducedHandles = { + head: 'top', + body: '', + tail: 'bottom', + }; const height = Math.round(this.invoke('getCellHeight')); return { - handles: DEFAULT_VERTICAL_HANDLES, + handles: this.option('reduced') ? reducedHandles[this.option('reduced') as any] : DEFAULT_VERTICAL_HANDLES, minWidth: 0, minHeight: height, step: height, diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.test.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.test.ts new file mode 100644 index 000000000000..170b05c4adfd --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from '@jest/globals'; +import { mockFieldExpressions } from '@ts/scheduler/__mock__/appointment_data_accessor.mock'; + +import subscribes from './m_subscribes'; +import type { ViewType } from './types'; +import { AppointmentDataAccessor } from './utils/data_accessor/appointment_data_accessor'; + +const CELL_WIDTH = 100; +const CELL_HEIGHT = 50; +const CELL_DURATION = 30; +const HOUR_MS = 3600000; +const DAY_MS = 24 * HOUR_MS; + +const createScheduler = (viewType: ViewType, allDayPanelMode = 'all'): unknown => ({ + currentView: { type: viewType }, + _dataAccessors: new AppointmentDataAccessor(mockFieldExpressions, true, 'yyyy/MM/dd HH:mm:ss'), + option: (name: string): unknown => (name === 'allDayPanelMode' ? allDayPanelMode : undefined), + getWorkSpace: () => ({ + getCellWidth: () => CELL_WIDTH, + getCellHeight: () => CELL_HEIGHT, + option: (name: string): unknown => (name === 'cellDuration' ? CELL_DURATION : undefined), + positionHelper: { getResizableStep: () => CELL_WIDTH }, + }), +}); + +const getDeltaTime = ( + viewType: ViewType, + allDay: boolean, + size: { width: number; height: number }, + allDayPanelMode = 'all', +): number => subscribes.getDeltaTime.call( + createScheduler(viewType, allDayPanelMode), + size, + { width: 0, height: 0 }, + { + startDate: new Date(2021, 3, 12, 9), + endDate: allDay ? new Date(2021, 3, 13, 9) : new Date(2021, 3, 12, 10), + allDay, + }, +); + +describe('getDeltaTime', () => { + describe('timeline views', () => { + it.each(['timelineDay', 'timelineWeek', 'timelineWorkWeek'] as ViewType[])( + 'should resize an all day appointment by the cell duration in %s', + (viewType) => { + expect(getDeltaTime(viewType, true, { width: CELL_WIDTH, height: 0 })) + .toBe(CELL_DURATION * 60000); + }, + ); + + it('should resize a regular appointment by the cell duration', () => { + expect(getDeltaTime('timelineWeek', false, { width: CELL_WIDTH, height: 0 })) + .toBe(CELL_DURATION * 60000); + }); + + it('should resize an all day appointment by whole days in timelineMonth', () => { + expect(getDeltaTime('timelineMonth', true, { width: CELL_WIDTH, height: 0 })) + .toBe(DAY_MS); + }); + }); + + describe('vertical views', () => { + it('should resize an all day appointment by whole days', () => { + expect(getDeltaTime('week', true, { width: CELL_WIDTH, height: 0 })).toBe(DAY_MS); + }); + + it('should resize an all day appointment by the cell duration when the all day panel is hidden', () => { + expect(getDeltaTime('week', true, { width: 0, height: CELL_HEIGHT }, 'hidden')) + .toBe(CELL_DURATION * 60000); + }); + + it('should resize a regular appointment by the cell duration', () => { + expect(getDeltaTime('week', false, { width: 0, height: CELL_HEIGHT })) + .toBe(CELL_DURATION * 60000); + }); + }); + + describe('month view', () => { + it('should resize an all day appointment by whole days', () => { + expect(getDeltaTime('month', true, { width: CELL_WIDTH, height: 0 })).toBe(DAY_MS); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index 93f5f76ba0ea..dca323492eee 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -32,6 +32,15 @@ const isAllDay = ( return adapter.allDay; }; +// NOTE: The all day panel exists in vertical views only. In the other views +// an all day appointment is rendered in the date table, so it is resized +// by the cell duration and not by whole days. +const isAllDayPanelAppointment = ( + scheduler: Scheduler, + appointmentData: SafeAppointment, +): boolean => VERTICAL_VIEW_TYPES.includes(scheduler.currentView.type) + && isAllDay(scheduler, appointmentData); + const subscribes = { isCurrentViewAgenda() { return this.currentView.type === 'agenda'; @@ -188,7 +197,7 @@ const subscribes = { }, cellDurationInMinutes: this.getWorkSpace().option('cellDuration'), resizableStep: this.getWorkSpace().positionHelper.getResizableStep(), - isAllDayPanel: isAllDay(this, itemData), + isAllDayPanel: isAllDayPanelAppointment(this, itemData), }); }, From 8e615d217b4e9b41965e633e26b73554c941dcce Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Sat, 8 Aug 2026 16:26:44 -0300 Subject: [PATCH 2/2] Scheduler: clarify the all day panel resizing note --- packages/devextreme/js/__internal/scheduler/m_subscribes.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts index dca323492eee..725fc96cfbd2 100644 --- a/packages/devextreme/js/__internal/scheduler/m_subscribes.ts +++ b/packages/devextreme/js/__internal/scheduler/m_subscribes.ts @@ -32,9 +32,9 @@ const isAllDay = ( return adapter.allDay; }; -// NOTE: The all day panel exists in vertical views only. In the other views -// an all day appointment is rendered in the date table, so it is resized -// by the cell duration and not by whole days. +// NOTE: Only in vertical views an all day appointment lives in the all day panel +// and is resized by whole days. In timeline views it is rendered in the date table +// and resized by the cell duration; month and timelineMonth are day-based anyway. const isAllDayPanelAppointment = ( scheduler: Scheduler, appointmentData: SafeAppointment,