From 21f38fa54bdde5f7e2a96b2035beff31c6936a04 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Tue, 19 May 2026 20:16:32 +0800 Subject: [PATCH 1/5] implement & tests --- .../__tests__/__mock__/model/scheduler.ts | 4 + .../__tests__/appointments_new.test.ts | 476 +++++++++++++++++- .../appointments/m_appointment_collection.ts | 1 - .../__mock__/appointment_collector.ts | 3 +- .../__mock__/base_appointment_view.ts | 1 + .../appointment/base_appointment.ts | 53 +- .../appointments_new/appointment_collector.ts | 31 +- .../appointments_new/appointments.test.ts | 6 + .../appointments_new/appointments.ts | 138 ++++- .../appointments_new/view_item.test.ts | 1 - .../scheduler/appointments_new/view_item.ts | 5 - .../js/__internal/scheduler/m_scheduler.ts | 51 +- .../tooltip_strategy_base.ts | 20 +- 13 files changed, 728 insertions(+), 62 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts index 8a59c6cd9f84..5f74e281cd85 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -146,6 +146,10 @@ export class SchedulerModel { return this.getPopups().length > 0; } + isRecurrenceDialogVisible(): boolean { + return !!document.querySelector(`.dx-overlay-wrapper.${POPUP_DIALOG_CLASS}`); + } + getPopups = (): NodeListOf => document.querySelectorAll(`.dx-overlay-wrapper.${APPOINTMENT_POPUP_CLASS}, .dx-overlay-wrapper.${POPUP_DIALOG_CLASS}`); getLoadPanel = (): HTMLElement | null => document.querySelector('.dx-loadpanel'); diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts index 426af8afe22f..909bae9154c4 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts @@ -25,6 +25,7 @@ describe('New Appointments', () => { // @ts-expect-error $scheduler.dxScheduler('dispose'); document.body.innerHTML = ''; + jest.useRealTimers(); }); describe('Options', () => { @@ -341,19 +342,33 @@ describe('New Appointments', () => { describe('onAppointmentRendered', () => { it('should call onAppointmentRendered callback', async () => { const onAppointmentRendered = jest.fn(); + const appointment = { + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }; - await createScheduler({ - dataSource: [{ - text: 'Appointment 1', - startDate: new Date(2015, 1, 9, 8), - endDate: new Date(2015, 1, 9, 9), - }], + const { POM, scheduler } = await createScheduler({ + dataSource: [appointment], currentView: 'day', currentDate: new Date(2015, 1, 9, 8), onAppointmentRendered, }); expect(onAppointmentRendered).toHaveBeenCalledTimes(1); + const callArg = onAppointmentRendered.mock.calls[0][0] as any; + expect(Object.keys(callArg).sort()).toEqual([ + 'appointmentData', 'appointmentElement', 'component', 'element', 'targetedAppointmentData', + ]); + expect(callArg.component).toBe(scheduler); + expect(callArg.element).toBe(scheduler.$element().get(0)); + expect(callArg.appointmentElement).toBe(POM.getAppointments()[0].element); + expect(callArg.appointmentData).toEqual(appointment); + expect(callArg.targetedAppointmentData).toEqual({ + ...appointment, + displayStartDate: new Date(2015, 1, 9, 8), + displayEndDate: new Date(2015, 1, 9, 9), + }); }); it('should call onAppointmentRendered after .option() change', async () => { @@ -375,4 +390,453 @@ describe('New Appointments', () => { expect(onAppointmentRendered).toHaveBeenCalledTimes(1); }); }); + + describe('onAppointmentClick', () => { + it('should call onAppointmentClick callback', async () => { + const onAppointmentClick = jest.fn(); + + const appointment = { + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }; + + const { POM, scheduler } = await createScheduler({ + dataSource: [appointment], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentClick, + }); + + POM.getAppointments()[0].element.click(); + + expect(onAppointmentClick).toHaveBeenCalledTimes(1); + const callArg = onAppointmentClick.mock.calls[0][0] as any; + expect(Object.keys(callArg).sort()).toEqual([ + 'appointmentData', 'appointmentElement', 'component', 'element', 'event', 'targetedAppointmentData', + ]); + expect(callArg.component).toBe(scheduler); + expect(callArg.element).toBe(scheduler.$element().get(0)); + expect(callArg.event.type).toBe('dxclick'); + expect(callArg.appointmentElement).toBe(POM.getAppointments()[0].element); + expect(callArg.appointmentData).toEqual(appointment); + expect(callArg.targetedAppointmentData).toEqual({ + ...appointment, + displayStartDate: new Date(2015, 1, 9, 8), + displayEndDate: new Date(2015, 1, 9, 9), + }); + }); + + it('should prevent tooltip showing when onAppointmentClick callback sets e.cancel = true', async () => { + const onAppointmentClick = jest.fn((e) => { + (e as any).cancel = true; + }); + + const { POM } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentClick, + }); + + POM.getAppointments()[0].element.click(); + + expect(onAppointmentClick).toHaveBeenCalledTimes(1); + expect(POM.tooltip.isVisible()).toBe(false); + }); + + it('should call onAppointmentClick after .option() change', async () => { + const { POM, scheduler } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + const onAppointmentClick = jest.fn(); + scheduler.option('onAppointmentClick', onAppointmentClick); + + POM.getAppointments()[0].element.click(); + + expect(onAppointmentClick).toHaveBeenCalledTimes(1); + }); + + it('should not call onAppointmentClick on collector click', async () => { + const onAppointmentClick = jest.fn(); + + const { POM } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 1, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentClick, + }); + + POM.getCollectorButton().click(); + + expect(onAppointmentClick).not.toHaveBeenCalled(); + }); + + it('should call onAppointmentClick on tooltip item click', async () => { + const onAppointmentClick = jest.fn(); + + const { POM } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 1, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentClick, + }); + + POM.getCollectorButton().click(); + + POM.tooltip.getAppointmentItem(0).click(); + + expect(onAppointmentClick).toHaveBeenCalledTimes(1); + }); + + it('should call onAppointmentClick on tooltip item click after .option() change', async () => { + const { POM, scheduler } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 1, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + const onAppointmentClick = jest.fn(); + scheduler.option('onAppointmentClick', onAppointmentClick); + + jest.useFakeTimers(); + POM.getCollectorButton().click(); + jest.runAllTimers(); + + POM.tooltip.getAppointmentItem(0).click(); + + expect(onAppointmentClick).toHaveBeenCalledTimes(1); + }); + }); + + describe('onAppointmentDblClick', () => { + it('should call onAppointmentDblClick callback', async () => { + const onAppointmentDblClick = jest.fn(); + + const appointment = { + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }; + + const { scheduler, POM } = await createScheduler({ + dataSource: [appointment], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentDblClick, + }); + + POM.openPopupByDblClick('Appointment 1'); + + expect(onAppointmentDblClick).toHaveBeenCalledTimes(1); + const callArg = onAppointmentDblClick.mock.calls[0][0] as any; + expect(Object.keys(callArg).sort()).toEqual([ + 'appointmentData', 'appointmentElement', 'component', 'element', 'event', 'targetedAppointmentData', + ]); + expect(callArg.component).toBe(scheduler); + expect(callArg.element).toBe(scheduler.$element().get(0)); + expect(callArg.event.type).toBe('dxdblclick'); + expect(callArg.appointmentElement).toBe(POM.getAppointments()[0].element); + expect(callArg.appointmentData).toEqual(appointment); + expect(callArg.targetedAppointmentData).toEqual({ + ...appointment, + displayStartDate: new Date(2015, 1, 9, 8), + displayEndDate: new Date(2015, 1, 9, 9), + }); + }); + + it('should prevent appointment popup showing when onAppointmentDblClick callback sets e.cancel = true', async () => { + const onAppointmentDblClick = jest.fn((e) => { + (e as any).cancel = true; + }); + + const { POM } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentDblClick, + }); + + POM.openPopupByDblClick('Appointment 1'); + + expect(onAppointmentDblClick).toHaveBeenCalledTimes(1); + expect(POM.isPopupVisible()).toBe(false); + }); + + it('should call onAppointmentDblClick after .option() change', async () => { + const { POM, scheduler } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + const onAppointmentDblClick = jest.fn(); + scheduler.option('onAppointmentDblClick', onAppointmentDblClick); + + POM.openPopupByDblClick('Appointment 1'); + + expect(onAppointmentDblClick).toHaveBeenCalledTimes(1); + }); + + it('should not call onAppointmentDblClick on collector double click', async () => { + const onAppointmentDblClick = jest.fn(); + + const { POM } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 2, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentDblClick, + }); + + onAppointmentDblClick.mockClear(); + const collector = POM.getCollectorButton(); + collector.click(); + collector.click(); + + expect(onAppointmentDblClick).not.toHaveBeenCalled(); + }); + }); + + describe('Tooltip', () => { + it('should show tooltip on appointment click', async () => { + const { POM } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + jest.useFakeTimers(); + POM.getAppointments()[0].element.click(); + jest.runAllTimers(); + + expect(POM.tooltip.isVisible()).toBe(true); + expect(POM.tooltip.getAppointmentItems().length).toBe(1); + expect(POM.tooltip.getAppointmentItem(0).textContent).toContain('Appointment 1'); + }); + + it('should show tooltip on collector click', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 1, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + jest.useFakeTimers(); + POM.getCollectorButton().click(); + jest.runAllTimers(); + + expect(POM.tooltip.isVisible()).toBe(true); + expect(POM.tooltip.getAppointmentItems().length).toBe(2); + expect(POM.tooltip.getAppointmentItem(0).textContent).toContain('Appointment 2'); + expect(POM.tooltip.getAppointmentItem(1).textContent).toContain('Appointment 3'); + }); + }); + + describe('Appointment Popup', () => { + it('should show appointment popup on appointment double click', async () => { + const { POM } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + POM.openPopupByDblClick('Appointment 1'); + + expect(POM.tooltip.isVisible()).toBe(false); + expect(POM.isPopupVisible()).toBe(true); + }); + + it('should not show appointment popup on collector double click', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 1, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + const collector = POM.getCollectorButton(); + collector.click(); + collector.click(); + + expect(POM.isPopupVisible()).toBe(false); + }); + + it('should show appointment popup on tooltip item click', async () => { + const { POM } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 2, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + jest.useFakeTimers(); + POM.getCollectorButton().click(); + jest.runAllTimers(); + + POM.tooltip.getAppointmentItem(0).click(); + + expect(POM.isPopupVisible()).toBe(true); + }); + + it('should show recurrence dialog on recurrence appointment double click', async () => { + const appointment = { + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + recurrenceRule: 'FREQ=DAILY', + }; + + const { POM } = await createScheduler({ + dataSource: [appointment], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + POM.openPopupByDblClick('Appointment 1'); + + expect(POM.isRecurrenceDialogVisible()).toBe(true); + }); + + it('should have correct data in appointment popup', async () => { + const appointmentData = { + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }; + + const { POM } = await createScheduler({ + dataSource: [appointmentData], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + }); + + POM.openPopupByDblClick('Appointment 1'); + + expect(POM.isPopupVisible()).toBe(true); + expect(POM.popup.getInputValue('subjectEditor')).toBe('Appointment 1'); + }); + + it('should save new appointment data after saving changes', async () => { + const onAppointmentUpdated = jest.fn(); + + const appointment = { + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }; + + const { POM } = await createScheduler({ + dataSource: [appointment], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentUpdated, + }); + + POM.openPopupByDblClick('Appointment 1'); + + POM.popup.setInputValue('subjectEditor', 'Updated Appointment'); + POM.popup.saveButton.click(); + await new Promise(process.nextTick); + + expect(onAppointmentUpdated).toHaveBeenCalledTimes(1); + expect((onAppointmentUpdated.mock.calls[0][0] as any).appointmentData).toBe(appointment); + expect(appointment.text).toBe('Updated Appointment'); + }); + + it('should save appointment data after saving changes from tooltip', async () => { + const onAppointmentUpdated = jest.fn(); + + const appointment = { + text: 'Appointment 2', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }; + + const { POM } = await createScheduler({ + dataSource: [ + { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + appointment, + { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, + ], + maxAppointmentsPerCell: 1, + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentUpdated, + }); + + jest.useFakeTimers(); + POM.getCollectorButton().click(); + jest.runAllTimers(); + jest.useRealTimers(); + + POM.tooltip.getAppointmentItem(0).click(); + POM.popup.setInputValue('subjectEditor', 'Updated Appointment'); + POM.popup.saveButton.click(); + await new Promise(process.nextTick); + + expect(onAppointmentUpdated).toHaveBeenCalledTimes(1); + expect((onAppointmentUpdated.mock.calls[0][0] as any).appointmentData).toBe(appointment); + expect(appointment.text).toBe('Updated Appointment'); + }); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts index 62d26aeb6483..4a4ecf951a71 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -1058,7 +1058,6 @@ class SchedulerAppointments extends CollectionWidget { const appointmentConfig = { itemData: item.itemData, groupIndex: appointment.groupIndex, - groups: this.option('groups'), }; return { diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/appointment_collector.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/appointment_collector.ts index ad3aa5f572de..8b223b1f1c51 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/appointment_collector.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/appointment_collector.ts @@ -4,6 +4,7 @@ import type { SafeAppointment, TargetedAppointment } from '@ts/scheduler/types'; import type { AppointmentCollectorProperties } from '../appointment_collector'; import { AppointmentCollector } from '../appointment_collector'; +import { mockGridViewModel } from './appointment_view_model'; export const getAppointmentCollectorProperties = ( appointmentsData: SafeAppointment[], @@ -17,7 +18,7 @@ export const getAppointmentCollectorProperties = ( const config: AppointmentCollectorProperties = { tabIndex: 0, sortedIndex: 0, - appointmentsData, + items: appointmentsData.map((item) => mockGridViewModel(item)), isCompact: false, geometry: { height: 30, diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/base_appointment_view.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/base_appointment_view.ts index 2f89a95bedf2..f9cc4053286f 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/base_appointment_view.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/__mock__/base_appointment_view.ts @@ -27,6 +27,7 @@ export const getBaseAppointmentViewProperties = ( onFocusIn: () => {}, onFocusOut: () => {}, onClick: () => {}, + onDblClick: () => {}, onKeyDown: () => {}, getDataAccessor: (): AppointmentDataAccessor => mockAppointmentDataAccessor, getResourceColor: (): Promise => Promise.resolve(undefined), diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment/base_appointment.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment/base_appointment.ts index 1cc60c5ef089..782f8a87be0a 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment/base_appointment.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment/base_appointment.ts @@ -1,14 +1,16 @@ import messageLocalization from '@js/common/core/localization/message'; import registerComponent from '@js/core/component_registrator'; -import type { DxElement } from '@js/core/element'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { DxEvent } from '@js/events'; +import eventsEngine from '@js/events/core/events_engine'; +import { addNamespace } from '@js/events/utils'; +import type { AppointmentRenderedEvent } from '@js/ui/scheduler'; import { getPublicElement } from '@ts/core/m_element'; import { EmptyTemplate } from '@ts/core/templates/m_empty_template'; import { FunctionTemplate } from '@ts/core/templates/m_function_template'; import type { TemplateBase } from '@ts/core/templates/m_template_base'; -import { click } from '@ts/events/m_short'; +import { dxClick } from '@ts/events/m_short'; import type { SafeAppointment, TargetedAppointment } from '@ts/scheduler/types'; import type { AppointmentDataAccessor } from '@ts/scheduler/utils/data_accessor/appointment_data_accessor'; @@ -16,6 +18,8 @@ import { APPOINTMENT_CLASSES, APPOINTMENT_TYPE_CLASSES, FOCUSED_STATE_CLASS } fr import { DateFormatType, getDateTextFromTargetAppointment } from '../utils/get_date_text'; import { EVENTS_NAMESPACE, ViewItem, type ViewItemProperties } from '../view_item'; +const DOUBLE_CLICK_EVENT_NAME = addNamespace('dxdblclick', EVENTS_NAMESPACE.namespace); + export interface BaseAppointmentViewProperties extends ViewItemProperties { index: number; @@ -23,11 +27,9 @@ export interface BaseAppointmentViewProperties targetedAppointmentData: TargetedAppointment; appointmentTemplate: TemplateBase; - onRendered: (e: { - element: DxElement; - appointmentData: SafeAppointment; - targetedAppointmentData: TargetedAppointment; - }) => void; + onRendered: (e: AppointmentRenderedEvent) => void; + onClick: (appointmentView: BaseAppointmentView, event: DxEvent) => void; + onDblClick: (appointmentView: BaseAppointmentView, event: DxEvent) => void; getDataAccessor: () => AppointmentDataAccessor; getResourceColor: () => Promise; @@ -36,16 +38,27 @@ export interface BaseAppointmentViewProperties export class BaseAppointmentView< TProperties extends BaseAppointmentViewProperties = BaseAppointmentViewProperties, > extends ViewItem { - protected get targetedAppointmentData(): TargetedAppointment { + get targetedAppointmentData(): TargetedAppointment { return this.option().targetedAppointmentData; } - protected get appointmentData(): SafeAppointment { + get appointmentData(): SafeAppointment { return this.option().appointmentData; } private defaultAppointmentTemplate!: FunctionTemplate; + override _setOptionsByReference(): void { + super._setOptionsByReference(); + + // Note: appointmentData object is used as a key in dataSource + this._optionsByReference = { + ...this._optionsByReference, + appointmentData: true, + targetedAppointmentData: true, + }; + } + override _init(): void { super._init(); @@ -62,6 +75,7 @@ export class BaseAppointmentView< this.applyAria(); this.attachFocusEvents(); this.attachClickEvent(); + this.attachDblClickEvent(); this.attachKeydownEvents(); this.renderContentTemplate(); } @@ -69,7 +83,8 @@ export class BaseAppointmentView< override _dispose(): void { super._dispose(); - click.off(this.$element(), EVENTS_NAMESPACE); + dxClick.off(this.$element(), EVENTS_NAMESPACE); + eventsEngine.off(this.$element(), DOUBLE_CLICK_EVENT_NAME); } protected applyElementClasses(): void { @@ -86,14 +101,23 @@ export class BaseAppointmentView< } private attachClickEvent(): void { - click.off(this.$element(), EVENTS_NAMESPACE); - click.on( + dxClick.off(this.$element(), EVENTS_NAMESPACE); + dxClick.on( this.$element(), - this.onClick.bind(this), + (event: DxEvent) => this.option().onClick(this, event), EVENTS_NAMESPACE, ); } + private attachDblClickEvent(): void { + eventsEngine.off(this.$element(), DOUBLE_CLICK_EVENT_NAME); + eventsEngine.on( + this.$element(), + DOUBLE_CLICK_EVENT_NAME, + (event: DxEvent) => this.option().onDblClick(this, event), + ); + } + protected override onFocusIn(): void { this.$element().addClass(FOCUSED_STATE_CLASS); @@ -163,8 +187,9 @@ export class BaseAppointmentView< }, index: this.option().index, onRendered: () => { + // @ts-expect-error 'component' and 'element' are set by action this.option().onRendered({ - element: getPublicElement(this.$element()), + appointmentElement: getPublicElement(this.$element()), appointmentData: this.appointmentData, targetedAppointmentData: this.targetedAppointmentData, }); diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts index 61d4a0e90d7a..dcb9a693b7a9 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts @@ -4,18 +4,21 @@ import registerComponent from '@js/core/component_registrator'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { EmptyTemplate } from '@js/core/templates/empty_template'; +import type { DxEvent } from '@js/events'; import Button from '@js/ui/button'; +import type { ButtonClickEvent } from '@js/ui/drop_down_button'; import { FunctionTemplate } from '@ts/core/templates/m_function_template'; import type { TemplateBase } from '@ts/core/templates/m_template_base'; import type { SafeAppointment, TargetedAppointment } from '@ts/scheduler/types'; +import type { AppointmentItemViewModel } from '../view_model/types'; import { APPOINTMENT_COLLECTOR_CLASSES } from './const'; import type { ViewItemProperties } from './view_item'; import { ViewItem } from './view_item'; export interface AppointmentCollectorProperties extends ViewItemProperties { - appointmentsData: SafeAppointment[]; + items: AppointmentItemViewModel[], isCompact: boolean; geometry: { height: number; @@ -25,16 +28,32 @@ export interface AppointmentCollectorProperties }; targetedAppointmentData: TargetedAppointment; appointmentCollectorTemplate: TemplateBase; + onClick: (viewItem: AppointmentCollector, e: DxEvent) => void; } export class AppointmentCollector extends ViewItem { + private appointmentsData!: SafeAppointment[]; + private defaultAppointmentCollectorTemplate!: FunctionTemplate; private buttonInstance?: Button; private get appointmentsCount(): number { - return this.option().appointmentsData.length; + return this.option().items.length; + } + + override _setOptionsByReference(): void { + super._setOptionsByReference(); + + // Note: appointmentData object is used as a key in dataSource + this._optionsByReference = { + ...this._optionsByReference, + // @ts-expect-error + items: true, + // @ts-expect-error + targetedAppointmentData: true, + }; } override _init(): void { @@ -43,6 +62,8 @@ export class AppointmentCollector this.defaultAppointmentCollectorTemplate = new FunctionTemplate((options) => { this.defaultAppointmentCollectorContent($(options.container)); }); + + this.appointmentsData = this.option().items.map((item) => item.itemData); } override _initMarkup(): void { @@ -115,10 +136,12 @@ export class AppointmentCollector model: { appointmentCount: this.appointmentsCount, isCompact: this.option().isCompact, - items: this.option().appointmentsData, + items: this.appointmentsData, }, })), - onClick: this.onClick.bind(this), + onClick: (e: ButtonClickEvent) => { + this.option().onClick(this, e.event as DxEvent); + }, }); } diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts index d7686404f264..1262eb5c258a 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts @@ -39,6 +39,8 @@ const getProperties = (options: { appointmentCollectorTemplate: 'appointmentCollector', onAppointmentRendered: (): void => {}, + onAppointmentClick: (): void => {}, + onAppointmentDblClick: (): void => {}, getStartViewDate: () => new Date(2024, 0, 1), getSortedAppointments: () => [], @@ -48,6 +50,10 @@ const getProperties = (options: { getAppointmentDataSource: mockAppointmentDataSource, getResourceManager: () => getResourceManagerMock(options.resources ?? []), getDataAccessor: () => mockAppointmentDataAccessor, + + showAppointmentTooltip: (): void => {}, + showAppointmentTooltipCore: (): void => {}, + showEditAppointmentPopup: (): void => {}, }); const createAppointments = ( diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts index 263b4f4d9fc6..dc8c6596c90a 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts @@ -1,14 +1,23 @@ import registerComponent from '@js/core/component_registrator'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import type { Properties as SchedulerProperties } from '@js/ui/scheduler'; +import type { DxEvent } from '@js/events'; +import type { + AppointmentClickEvent, + AppointmentDblClickEvent, + AppointmentRenderedEvent, + Properties as SchedulerProperties, +} from '@js/ui/scheduler'; import { domAdapter } from '@ts/core/m_dom_adapter'; +import { getPublicElement } from '@ts/core/m_element'; import { EmptyTemplate } from '@ts/core/templates/m_empty_template'; +import { isElementInDom } from '@ts/core/utils/m_dom'; import type { DOMComponentProperties } from '@ts/core/widget/dom_component'; import DOMComponent from '@ts/core/widget/dom_component'; import type { OptionChanged } from '@ts/core/widget/types'; import type { + AppointmentTooltipItem, SafeAppointment, ScrollToOptions, TargetedAppointment, ViewType, } from '../types'; import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; @@ -23,7 +32,7 @@ import type { SortedEntity, } from '../view_model/types'; import { AgendaAppointmentView } from './appointment/agenda_appointment'; -import type { BaseAppointmentViewProperties } from './appointment/base_appointment'; +import type { BaseAppointmentView, BaseAppointmentViewProperties } from './appointment/base_appointment'; import { GridAppointmentView } from './appointment/grid_appointment'; import { AppointmentCollector } from './appointment_collector'; import { AppointmentsFocusController } from './appointments.focus_controller'; @@ -34,6 +43,8 @@ import { getViewModelDiff } from './utils/get_view_model_diff'; import { isAgendaAppointmentViewModel, isCollectorViewModel as isAppointmentCollectorViewModel, isGridAppointmentViewModel } from './utils/type_helpers'; import type { ViewItem } from './view_item'; +const SHOW_TOOLTIP_TIMEOUT = 300; + export interface AppointmentsProperties extends DOMComponentProperties { currentView: ViewType; tabIndex: number; @@ -44,7 +55,9 @@ export interface AppointmentsProperties extends DOMComponentProperties void; + onAppointmentClick: (e: AppointmentClickEvent) => void; + onAppointmentDblClick: (e: AppointmentDblClickEvent) => void; getAppointmentDataSource: () => AppointmentDataSource; getResourceManager: () => ResourceManager; @@ -52,12 +65,32 @@ export interface AppointmentsProperties extends DOMComponentProperties Date; getSortedAppointments: () => SortedEntity[]; isVirtualScrolling: () => boolean; + scrollTo: (date: Date, options?: ScrollToOptions) => void; + showAppointmentTooltip: ( + appointment: SafeAppointment, + $element: dxElementWrapper, + targetedAppointment?: SafeAppointment, + ) => void; + showAppointmentTooltipCore: ( + target: dxElementWrapper, + data: AppointmentTooltipItem[], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + options?: any, + ) => void; + showEditAppointmentPopup: ( + appointmentData: SafeAppointment, + targetedAppointmentData: TargetedAppointment, + ) => void; } export class Appointments extends DOMComponent { private focusController!: AppointmentsFocusController; + private appointmentClickTimeout: number | null = null; + + private preventSingleAppointmentClick = false; + private viewItemBySortedIndex: Record = {}; private viewItems: ViewItem[] = []; @@ -101,6 +134,8 @@ export class Appointments extends DOMComponent {}; + return { ...super._getDefaultOptions(), tabIndex: 0, @@ -108,7 +143,9 @@ export class Appointments extends DOMComponent {}, + onAppointmentRendered: noop, + onAppointmentClick: noop, + onAppointmentDblClick: noop, }; } @@ -279,14 +316,13 @@ export class Appointments extends DOMComponent item.itemData), + items: appointmentViewModel.items, isCompact: appointmentViewModel.isCompact, geometry: { height: appointmentViewModel.height, @@ -296,6 +332,7 @@ export class Appointments extends DOMComponent { + if (isElementInDom($target) && !this.preventSingleAppointmentClick) { + this.option().showAppointmentTooltip( + appointmentView.appointmentData, + $target, + appointmentView.targetedAppointmentData, + ); + } + + this.preventSingleAppointmentClick = false; + }, SHOW_TOOLTIP_TIMEOUT); + } + + private onAppointmentDblClick( + appointmentView: BaseAppointmentView, + event: DxEvent, + ): void { + const e = { + appointmentElement: getPublicElement(appointmentView.$element()), + appointmentData: appointmentView.appointmentData, + targetedAppointmentData: appointmentView.targetedAppointmentData, + event, + }; + + // @ts-expect-error 'component' and 'element' are set by action + this.option().onAppointmentDblClick(e); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((e as any).cancel) { + return; + } + + if (this.appointmentClickTimeout) { + clearTimeout(this.appointmentClickTimeout); + this.preventSingleAppointmentClick = true; + } + + this.option().showEditAppointmentPopup( + appointmentView.appointmentData, + appointmentView.targetedAppointmentData, + ); + } + + private onCollectorClick(collector: AppointmentCollector): void { + this.focusController.onViewItemClick(collector); + + const collectorTooltipItems = collector.option().items.map((appointmentViewModel) => ({ + appointment: appointmentViewModel.itemData, + targetedAppointment: this.getTargetedAppointmentData(appointmentViewModel), + color: this.getResourceColor(appointmentViewModel), + settings: appointmentViewModel, + })); + + this.option().showAppointmentTooltipCore( + collector.$element(), + collectorTooltipItems, + { + dragBehavior: undefined, // TODO + isButtonClick: true, + tabFocusLoopEnabled: true, + }, + ); + } } // TODO: rename to dxSchedulerAppointments when old impl is removed diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.test.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.test.ts index 948a3b49626d..72b1a73b8507 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.test.ts @@ -46,7 +46,6 @@ describe.each([ sortedIndex: 0, onFocusIn: () => {}, onFocusOut: () => {}, - onClick: () => {}, onKeyDown: () => {}, }; diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.ts index 56a6163fe1ad..780e74ece34b 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/view_item.ts @@ -13,7 +13,6 @@ export interface ViewItemProperties sortedIndex: number; onFocusIn: (sortedIndex: number) => void; onFocusOut: (e: DxEvent, sortedIndex: number) => void; - onClick: (viewItem: ViewItem) => void; onKeyDown: (viewItem: ViewItem, e: KeyboardKeyDownEvent) => void; } @@ -72,10 +71,6 @@ export class ViewItem< this.option().onFocusOut(e, this.option().sortedIndex); } - protected onClick(): void { - this.option().onClick(this); - } - private onKeyDown(e: KeyboardKeyDownEvent): void { this.option().onKeyDown(this, e); } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 8289fa26eac5..d7204657b5ad 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -30,7 +30,6 @@ import DataHelperMixin from '@js/data_helper'; import { custom as customDialog } from '@js/ui/dialog'; import type { Appointment, AppointmentTooltipShowingEvent, DayOfWeek, Occurrence, - Properties as SchedulerProperties, } from '@js/ui/scheduler'; import errors from '@js/ui/widget/ui.errors'; import { dateUtilsTs } from '@ts/core/utils/date'; @@ -230,8 +229,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { private timeZonesPromise!: Promise; - private appointmentRenderedAction!: SchedulerProperties['onAppointmentRendered']; - get timeZoneCalculator() { if (!this.timeZoneCalculatorInstance) { this.timeZoneCalculatorInstance = createTimeZoneCalculator(this.option('timeZone')); @@ -426,16 +423,24 @@ class Scheduler extends SchedulerOptionsBaseWidget { break; case 'onAppointmentRendered': if (this.option('_newAppointments')) { - this.createAppointmentRenderedAction(); + this.actions.onAppointmentRendered = this._createActionByOption('onAppointmentRendered'); } else { this._appointments.option('onItemRendered', this.getAppointmentRenderedAction()); } break; case 'onAppointmentClick': - this._appointments.option('onItemClick', this._createActionByOption(name)); + if (this.option('_newAppointments')) { + this.actions.onAppointmentClick = this._createActionByOption('onAppointmentClick'); + } else { + this._appointments.option('onItemClick', this._createActionByOption(name)); + } break; case 'onAppointmentDblClick': - this._appointments.option(name, this._createActionByOption(name)); + if (this.option('_newAppointments')) { + this.actions.onAppointmentDblClick = this._createActionByOption('onAppointmentDblClick'); + } else { + this._appointments.option(name, this._createActionByOption(name)); + } break; case 'onAppointmentContextMenu': this._appointments.option('onItemContextMenu', this._createActionByOption(name)); @@ -814,12 +819,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.resourceManager = new ResourceManager(this.option('resources')); this.notifyScheduler = new NotifyScheduler({ scheduler: this }); - - this.createAppointmentRenderedAction(); - } - - private createAppointmentRenderedAction() { - this.appointmentRenderedAction = this._createActionByOption('onAppointmentRendered'); } createAppointmentDataSource() { @@ -1009,6 +1008,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { onAppointmentFormOpening: this._createActionByOption('onAppointmentFormOpening'), onAppointmentTooltipShowing: this._createActionByOption('onAppointmentTooltipShowing'), onSelectionEnd: this._createActionByOption('onSelectionEnd'), + onAppointmentRendered: this._createActionByOption('onAppointmentRendered'), + onAppointmentClick: this._createActionByOption('onAppointmentClick'), + onAppointmentDblClick: this._createActionByOption('onAppointmentDblClick'), }; } @@ -1073,22 +1075,27 @@ class Scheduler extends SchedulerOptionsBaseWidget { currentView: this.option('currentView') as ViewType, appointmentTemplate: this.getViewOption('appointmentTemplate'), appointmentCollectorTemplate: this.getViewOption('appointmentCollectorTemplate'), - onAppointmentRendered: (e) => { - // @ts-expect-error 'component' property is set by action - this.appointmentRenderedAction({ - appointmentElement: e.element, - appointmentData: e.appointmentData, - targetedAppointmentData: e.targetedAppointmentData, - }); - }, + + onAppointmentRendered: (...args) => this.actions.onAppointmentRendered(...args), + onAppointmentClick: (...args) => this.actions.onAppointmentClick(...args), + onAppointmentDblClick: (...args) => this.actions.onAppointmentDblClick(...args), + getResourceManager: () => this.resourceManager, getAppointmentDataSource: () => this.appointmentDataSource, getDataAccessor: () => this._dataAccessors, getStartViewDate: () => this.getStartViewDate(), getSortedAppointments: () => this._layoutManager.sortedItems, - isVirtualScrolling: () => this.isVirtualScrolling(), + scrollTo: this.scrollTo.bind(this), + showAppointmentTooltip: this.showAppointmentTooltip.bind(this), + showAppointmentTooltipCore: this.showAppointmentTooltipCore.bind(this), + showEditAppointmentPopup: ( + appointmentData: SafeAppointment, + targetedAppointmentData: TargetedAppointment, + ) => { + this.showAppointmentPopup(appointmentData, false, targetedAppointmentData); + }, }; // @ts-expect-error this._appointments = this._createComponent('
', Appointments, appointmentsConfig); @@ -1209,6 +1216,8 @@ class Scheduler extends SchedulerOptionsBaseWidget { getAppointmentDisabled: (appointment) => this._dataAccessors.get('disabled', appointment), onItemContextMenu: that._createActionByOption('onAppointmentContextMenu'), createEventArgs: that._createEventArgs.bind(that), + newAppointments: Boolean(this.option('_newAppointments')), + onAppointmentClick: (...args) => this.actions.onAppointmentClick(...args), }; } diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts index 9e52fb916cfc..e26204f522cb 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts @@ -1,4 +1,4 @@ -import type { DxElement } from '@js/core/element'; +import { type DxElement } from '@js/core/element'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { FunctionTemplate } from '@js/core/templates/function_template'; @@ -13,7 +13,7 @@ import type { } from '@js/ui/list'; import type dxOverlay from '@js/ui/overlay'; import type { Properties as OverlayProperties } from '@js/ui/overlay'; -import type { Appointment, Properties as SchedulerProperties } from '@js/ui/scheduler'; +import type { Appointment, AppointmentClickEvent, Properties as SchedulerProperties } from '@js/ui/scheduler'; import { createPromise } from '@ts/core/utils/promise'; import List from '@ts/ui/list/list.edit'; import type Tooltip from '@ts/ui/m_tooltip'; @@ -63,6 +63,8 @@ interface AppointmentTooltipOptions { getAppointmentDisabled: (appointment: Appointment) => boolean | undefined; onItemContextMenu: (eventArgs: unknown) => void; createEventArgs: (e: ItemContextMenuEvent) => unknown; + newAppointments?: boolean; // TODO + onAppointmentClick: (e: AppointmentClickEvent) => void; } interface AppointmentTooltipExtraOptions { @@ -324,7 +326,19 @@ export abstract class TooltipStrategyBase { } this.hide(); - this.extraOptions?.clickEvent?.(e); + + if (this._options.newAppointments) { + // @ts-expect-error 'component' and 'element' are set by action + this._options.onAppointmentClick({ + appointmentElement: e.itemElement, + appointmentData: e.itemData.appointment, + targetedAppointmentData: e.itemData.targetedAppointment, + event: e.event, + }); + } else { + this.extraOptions?.clickEvent?.(e); + } + this._options.showAppointmentPopup( e.itemData.appointment, false, From 2e0ab20cbc9ae1becb6e8c2ee549462a43b3e227 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Tue, 19 May 2026 20:29:08 +0800 Subject: [PATCH 2/5] fix single appt tooltip item click --- .../__tests__/appointments_new.test.ts | 25 ++++++++++++++++++- .../js/__internal/scheduler/m_scheduler.ts | 1 + .../tooltip_strategy_base.ts | 16 ++++++------ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts index 909bae9154c4..9c4e3561553d 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts @@ -488,6 +488,29 @@ describe('New Appointments', () => { expect(onAppointmentClick).not.toHaveBeenCalled(); }); + it('should not call onAppointmentClick on tooltip item inside single appointment', async () => { + const onAppointmentClick = jest.fn(); + + const { POM } = await createScheduler({ + dataSource: [{ + text: 'Appointment 1', + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentView: 'day', + currentDate: new Date(2015, 1, 9, 8), + onAppointmentClick, + }); + + jest.useFakeTimers(); + POM.getAppointments()[0].element.click(); + jest.runAllTimers(); + + onAppointmentClick.mockClear(); + POM.tooltip.getAppointmentItem(0).click(); + expect(onAppointmentClick).toHaveBeenCalledTimes(0); + }); + it('should call onAppointmentClick on tooltip item click', async () => { const onAppointmentClick = jest.fn(); @@ -510,7 +533,7 @@ describe('New Appointments', () => { expect(onAppointmentClick).toHaveBeenCalledTimes(1); }); - it('should call onAppointmentClick on tooltip item click after .option() change', async () => { + it('should call onAppointmentClick on tooltip item inside collector click after .option() change', async () => { const { POM, scheduler } = await createScheduler({ dataSource: [ { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index d7204657b5ad..456a261832e4 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1221,6 +1221,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { }; } + // TODO: delete this method when old impl is removed _createEventArgs(e) { const config = { itemData: e.itemData.appointment, diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts index e26204f522cb..bcde9bed148d 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts @@ -328,13 +328,15 @@ export abstract class TooltipStrategyBase { this.hide(); if (this._options.newAppointments) { - // @ts-expect-error 'component' and 'element' are set by action - this._options.onAppointmentClick({ - appointmentElement: e.itemElement, - appointmentData: e.itemData.appointment, - targetedAppointmentData: e.itemData.targetedAppointment, - event: e.event, - }); + if (this.extraOptions?.isButtonClick) { + // @ts-expect-error 'component' and 'element' are set by action + this._options.onAppointmentClick({ + appointmentElement: e.itemElement, + appointmentData: e.itemData.appointment, + targetedAppointmentData: e.itemData.targetedAppointment, + event: e.event, + }); + } } else { this.extraOptions?.clickEvent?.(e); } From 3183cfb54d2f27967bdd10d3a72d7eece2e5c393 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Tue, 19 May 2026 20:41:09 +0800 Subject: [PATCH 3/5] apply copilot's review --- .../scheduler/appointments_new/appointment_collector.ts | 4 ++-- .../__internal/scheduler/appointments_new/appointments.ts | 8 ++++++++ .../common.events.tests.js | 5 ++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts index dcb9a693b7a9..fe8a9fad4642 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts @@ -5,8 +5,8 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { EmptyTemplate } from '@js/core/templates/empty_template'; import type { DxEvent } from '@js/events'; +import type { ClickEvent as ButtonClickEvent } from '@js/ui/button'; import Button from '@js/ui/button'; -import type { ButtonClickEvent } from '@js/ui/drop_down_button'; import { FunctionTemplate } from '@ts/core/templates/m_function_template'; import type { TemplateBase } from '@ts/core/templates/m_template_base'; import type { SafeAppointment, TargetedAppointment } from '@ts/scheduler/types'; @@ -46,7 +46,7 @@ export class AppointmentCollector override _setOptionsByReference(): void { super._setOptionsByReference(); - // Note: appointmentData object is used as a key in dataSource + // Note: items have appointmentData, which is used as a key in dataSource this._optionsByReference = { ...this._optionsByReference, // @ts-expect-error diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts index dc8c6596c90a..a5ba370c42f0 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts @@ -127,6 +127,14 @@ export class Appointments extends DOMComponent Date: Thu, 21 May 2026 20:04:02 +0800 Subject: [PATCH 4/5] apply review & move some tests to unit --- .../__tests__/appointments_new.test.ts | 106 -------- .../appointments_new/appointment_collector.ts | 12 +- .../appointments_new/appointments.test.ts | 234 +++++++++++++++++- .../appointments_new/appointments.ts | 42 ++-- .../js/__internal/scheduler/m_scheduler.ts | 11 +- .../tooltip_strategy_base.ts | 2 +- 6 files changed, 264 insertions(+), 143 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts index 9c4e3561553d..ba81f77f6b1c 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts @@ -427,28 +427,6 @@ describe('New Appointments', () => { }); }); - it('should prevent tooltip showing when onAppointmentClick callback sets e.cancel = true', async () => { - const onAppointmentClick = jest.fn((e) => { - (e as any).cancel = true; - }); - - const { POM } = await createScheduler({ - dataSource: [{ - text: 'Appointment 1', - startDate: new Date(2015, 1, 9, 8), - endDate: new Date(2015, 1, 9, 9), - }], - currentView: 'day', - currentDate: new Date(2015, 1, 9, 8), - onAppointmentClick, - }); - - POM.getAppointments()[0].element.click(); - - expect(onAppointmentClick).toHaveBeenCalledTimes(1); - expect(POM.tooltip.isVisible()).toBe(false); - }); - it('should call onAppointmentClick after .option() change', async () => { const { POM, scheduler } = await createScheduler({ dataSource: [{ @@ -468,26 +446,6 @@ describe('New Appointments', () => { expect(onAppointmentClick).toHaveBeenCalledTimes(1); }); - it('should not call onAppointmentClick on collector click', async () => { - const onAppointmentClick = jest.fn(); - - const { POM } = await createScheduler({ - dataSource: [ - { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - ], - maxAppointmentsPerCell: 1, - currentView: 'day', - currentDate: new Date(2015, 1, 9, 8), - onAppointmentClick, - }); - - POM.getCollectorButton().click(); - - expect(onAppointmentClick).not.toHaveBeenCalled(); - }); - it('should not call onAppointmentClick on tooltip item inside single appointment', async () => { const onAppointmentClick = jest.fn(); @@ -594,28 +552,6 @@ describe('New Appointments', () => { }); }); - it('should prevent appointment popup showing when onAppointmentDblClick callback sets e.cancel = true', async () => { - const onAppointmentDblClick = jest.fn((e) => { - (e as any).cancel = true; - }); - - const { POM } = await createScheduler({ - dataSource: [{ - text: 'Appointment 1', - startDate: new Date(2015, 1, 9, 8), - endDate: new Date(2015, 1, 9, 9), - }], - currentView: 'day', - currentDate: new Date(2015, 1, 9, 8), - onAppointmentDblClick, - }); - - POM.openPopupByDblClick('Appointment 1'); - - expect(onAppointmentDblClick).toHaveBeenCalledTimes(1); - expect(POM.isPopupVisible()).toBe(false); - }); - it('should call onAppointmentDblClick after .option() change', async () => { const { POM, scheduler } = await createScheduler({ dataSource: [{ @@ -634,29 +570,6 @@ describe('New Appointments', () => { expect(onAppointmentDblClick).toHaveBeenCalledTimes(1); }); - - it('should not call onAppointmentDblClick on collector double click', async () => { - const onAppointmentDblClick = jest.fn(); - - const { POM } = await createScheduler({ - dataSource: [ - { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - ], - maxAppointmentsPerCell: 2, - currentView: 'day', - currentDate: new Date(2015, 1, 9, 8), - onAppointmentDblClick, - }); - - onAppointmentDblClick.mockClear(); - const collector = POM.getCollectorButton(); - collector.click(); - collector.click(); - - expect(onAppointmentDblClick).not.toHaveBeenCalled(); - }); }); describe('Tooltip', () => { @@ -721,25 +634,6 @@ describe('New Appointments', () => { expect(POM.isPopupVisible()).toBe(true); }); - it('should not show appointment popup on collector double click', async () => { - const { POM } = await createScheduler({ - dataSource: [ - { text: 'Appointment 1', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - { text: 'Appointment 2', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - { text: 'Appointment 3', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) }, - ], - maxAppointmentsPerCell: 1, - currentView: 'day', - currentDate: new Date(2015, 1, 9, 8), - }); - - const collector = POM.getCollectorButton(); - collector.click(); - collector.click(); - - expect(POM.isPopupVisible()).toBe(false); - }); - it('should show appointment popup on tooltip item click', async () => { const { POM } = await createScheduler({ dataSource: [ diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts index fe8a9fad4642..8fb0e262b724 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts @@ -9,7 +9,7 @@ import type { ClickEvent as ButtonClickEvent } from '@js/ui/button'; import Button from '@js/ui/button'; import { FunctionTemplate } from '@ts/core/templates/m_function_template'; import type { TemplateBase } from '@ts/core/templates/m_template_base'; -import type { SafeAppointment, TargetedAppointment } from '@ts/scheduler/types'; +import type { TargetedAppointment } from '@ts/scheduler/types'; import type { AppointmentItemViewModel } from '../view_model/types'; import { APPOINTMENT_COLLECTOR_CLASSES } from './const'; @@ -33,8 +33,6 @@ export interface AppointmentCollectorProperties export class AppointmentCollector extends ViewItem { - private appointmentsData!: SafeAppointment[]; - private defaultAppointmentCollectorTemplate!: FunctionTemplate; private buttonInstance?: Button; @@ -49,9 +47,9 @@ export class AppointmentCollector // Note: items have appointmentData, which is used as a key in dataSource this._optionsByReference = { ...this._optionsByReference, - // @ts-expect-error + // @ts-expect-error Component class has wrong type for _optionsByReference items: true, - // @ts-expect-error + // @ts-expect-error Component class has wrong type for _optionsByReference targetedAppointmentData: true, }; } @@ -62,8 +60,6 @@ export class AppointmentCollector this.defaultAppointmentCollectorTemplate = new FunctionTemplate((options) => { this.defaultAppointmentCollectorContent($(options.container)); }); - - this.appointmentsData = this.option().items.map((item) => item.itemData); } override _initMarkup(): void { @@ -136,7 +132,7 @@ export class AppointmentCollector model: { appointmentCount: this.appointmentsCount, isCompact: this.option().isCompact, - items: this.appointmentsData, + items: this.option().items.map((item) => item.itemData), }, })), onClick: (e: ButtonClickEvent) => { diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts index 1262eb5c258a..cd89a9e3cebe 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, jest, } from '@jest/globals'; import $ from '@js/core/renderer'; +import { fireEvent } from '@testing-library/dom'; import fx from '../../../common/core/animation/fx'; import { mockAppointmentDataAccessor } from '../__mock__/appointment_data_accessor.mock'; @@ -51,8 +52,8 @@ const getProperties = (options: { getResourceManager: () => getResourceManagerMock(options.resources ?? []), getDataAccessor: () => mockAppointmentDataAccessor, - showAppointmentTooltip: (): void => {}, - showAppointmentTooltipCore: (): void => {}, + showTooltipForAppointment: (): void => {}, + showTooltipForCollector: (): void => {}, showEditAppointmentPopup: (): void => {}, }); @@ -71,6 +72,12 @@ const defaultAppointmentData = { endDate: new Date(2024, 0, 1, 10, 0), }; +const dblClick = (element: HTMLElement): void => { + element.click(); + element.click(); + fireEvent(element, new Event('dxdblclick', { bubbles: true })); +}; + describe('Appointments', () => { beforeEach(() => { fx.off = true; @@ -897,12 +904,15 @@ describe('Appointments', () => { mockGridViewModel(defaultAppointmentData, { sortedIndex: 0 }), ]); + const element = instance.getViewItemBySortedIndex(0)?.$element().get(0); + expect(onAppointmentRendered).toHaveBeenCalledTimes(1); expect(onAppointmentRendered).toHaveBeenCalledWith( expect.objectContaining({ + appointmentElement: element, appointmentData: defaultAppointmentData, targetedAppointmentData: expect.objectContaining({ - text: defaultAppointmentData.text, + ...defaultAppointmentData, }), }), ); @@ -918,12 +928,15 @@ describe('Appointments', () => { mockAgendaViewModel(defaultAppointmentData, { sortedIndex: 0 }), ]); + const element = instance.getViewItemBySortedIndex(0)?.$element().get(0); + expect(onAppointmentRendered).toHaveBeenCalledTimes(1); expect(onAppointmentRendered).toHaveBeenCalledWith( expect.objectContaining({ + appointmentElement: element, appointmentData: defaultAppointmentData, targetedAppointmentData: expect.objectContaining({ - text: defaultAppointmentData.text, + ...defaultAppointmentData, }), }), ); @@ -972,4 +985,217 @@ describe('Appointments', () => { ); }); }); + + describe('onAppointmentClick', () => { + it('should not call onAppointmentClick on collector click', () => { + const onAppointmentClick = jest.fn(); + const instance = createAppointments({ + ...getProperties(), + onAppointmentClick, + }); + instance.option('viewModel', [ + mockAppointmentCollectorViewModel(defaultAppointmentData, { sortedIndex: 0 }), + ]); + + const viewItem = instance.getViewItemBySortedIndex(0); + const element = viewItem?.$element().get(0) as HTMLElement; + + element.click(); + + expect(onAppointmentClick).not.toHaveBeenCalledTimes(1); + }); + + it('should prevent tooltip showing when onAppointmentClick callback sets e.cancel = true', () => { + const onAppointmentClick = jest.fn((e) => { (e as any).cancel = true; }); + const showTooltipForAppointment = jest.fn(); + const showTooltipForCollector = jest.fn(); + + const instance = createAppointments({ + ...getProperties(), + onAppointmentClick, + showTooltipForAppointment, + showTooltipForCollector, + }); + instance.option('viewModel', [ + mockGridViewModel(defaultAppointmentData, { sortedIndex: 0 }), + ]); + + const viewItem = instance.getViewItemBySortedIndex(0); + const element = viewItem?.$element().get(0) as HTMLElement; + + jest.useFakeTimers(); + element.click(); + jest.runAllTimers(); + + expect(onAppointmentClick).toHaveBeenCalledTimes(1); + expect(showTooltipForAppointment).not.toHaveBeenCalled(); + expect(showTooltipForCollector).not.toHaveBeenCalled(); + }); + + it('should show tooltip correctly when two appointments are clicked one after another quickly', () => { + const showTooltipForAppointment = jest.fn(); + + const instance = createAppointments({ + ...getProperties(), + showTooltipForAppointment, + }); + instance.option('viewModel', [ + mockGridViewModel({ ...defaultAppointmentData, text: 'Appointment 1' }, { sortedIndex: 0 }), + mockGridViewModel({ ...defaultAppointmentData, text: 'Appointment 2' }, { sortedIndex: 1 }), + ]); + + const viewItem1 = instance.getViewItemBySortedIndex(0); + const element1 = viewItem1?.$element().get(0) as HTMLElement; + + const viewItem2 = instance.getViewItemBySortedIndex(1); + const element2 = viewItem2?.$element().get(0) as HTMLElement; + + jest.useFakeTimers(); + element1.click(); + element2.click(); + jest.runAllTimers(); + + expect(showTooltipForAppointment).toHaveBeenCalledTimes(1); + expect(showTooltipForAppointment).toHaveBeenCalledWith( + expect.objectContaining({ text: 'Appointment 2' }), + $(element2), + expect.objectContaining({ text: 'Appointment 2' }), + ); + }); + }); + + describe('onAppointmentDblClick', () => { + it('should call onAppointmentDblClick on appointment double click', () => { + const onAppointmentDblClick = jest.fn(); + const instance = createAppointments({ + ...getProperties(), + onAppointmentDblClick, + }); + instance.option('viewModel', [ + mockGridViewModel(defaultAppointmentData, { sortedIndex: 0 }), + ]); + + const viewItem = instance.getViewItemBySortedIndex(0); + const element = viewItem?.$element().get(0) as HTMLElement; + + jest.useFakeTimers(); + dblClick(element); + jest.runAllTimers(); + + expect(onAppointmentDblClick).toHaveBeenCalledTimes(1); + expect(onAppointmentDblClick).toHaveBeenCalledWith( + expect.objectContaining({ + appointmentElement: element, + appointmentData: defaultAppointmentData, + targetedAppointmentData: expect.objectContaining({ + ...defaultAppointmentData, + }), + event: expect.objectContaining({ type: 'dxdblclick' }), + }), + ); + }); + + it('should not call onAppointmentDblClick on collector double click', () => { + const onAppointmentDblClick = jest.fn(); + const instance = createAppointments({ + ...getProperties(), + onAppointmentDblClick, + }); + instance.option('viewModel', [ + mockAppointmentCollectorViewModel(defaultAppointmentData, { sortedIndex: 0 }), + ]); + + const viewItem = instance.getViewItemBySortedIndex(0); + const element = viewItem?.$element().get(0) as HTMLElement; + + jest.useFakeTimers(); + dblClick(element); + jest.runAllTimers(); + + expect(onAppointmentDblClick).not.toHaveBeenCalled(); + }); + + it('should show appointment popup on appointment double click', () => { + const showEditAppointmentPopup = jest.fn(); + const showTooltipForAppointment = jest.fn(); + const showTooltipForCollector = jest.fn(); + + const instance = createAppointments({ + ...getProperties(), + showEditAppointmentPopup, + showTooltipForAppointment, + showTooltipForCollector, + }); + instance.option('viewModel', [ + mockGridViewModel(defaultAppointmentData, { sortedIndex: 0 }), + ]); + + const viewItem = instance.getViewItemBySortedIndex(0); + const element = viewItem?.$element().get(0) as HTMLElement; + + jest.useFakeTimers(); + dblClick(element); + jest.runAllTimers(); + + expect(showEditAppointmentPopup).toHaveBeenCalledTimes(1); + expect(showEditAppointmentPopup).toHaveBeenCalledWith( + defaultAppointmentData, + expect.objectContaining({ + ...defaultAppointmentData, + }), + ); + expect(showTooltipForAppointment).not.toHaveBeenCalled(); + expect(showTooltipForCollector).not.toHaveBeenCalled(); + }); + + it('should not show appointment popup on collector double click', () => { + const showEditAppointmentPopup = jest.fn(); + const instance = createAppointments({ + ...getProperties(), + showEditAppointmentPopup, + }); + instance.option('viewModel', [ + mockAppointmentCollectorViewModel(defaultAppointmentData, { sortedIndex: 0 }), + ]); + + const viewItem = instance.getViewItemBySortedIndex(0); + const element = viewItem?.$element().get(0) as HTMLElement; + + jest.useFakeTimers(); + dblClick(element); + jest.runAllTimers(); + + expect(showEditAppointmentPopup).not.toHaveBeenCalled(); + }); + + it('should not show tooltip or appointment popup if onAppointmentDblClick sets e.cancel', () => { + const onAppointmentDblClick = jest.fn((e) => { (e as any).cancel = true; }); + const showEditAppointmentPopup = jest.fn(); + const showTooltipForAppointment = jest.fn(); + const showTooltipForCollector = jest.fn(); + + const instance = createAppointments({ + ...getProperties(), + onAppointmentDblClick, + showEditAppointmentPopup, + showTooltipForAppointment, + showTooltipForCollector, + }); + instance.option('viewModel', [ + mockGridViewModel(defaultAppointmentData, { sortedIndex: 0 }), + ]); + + const viewItem = instance.getViewItemBySortedIndex(0); + const element = viewItem?.$element().get(0) as HTMLElement; + + jest.useFakeTimers(); + dblClick(element); + jest.runAllTimers(); + + expect(onAppointmentDblClick).toHaveBeenCalledTimes(1); + expect(showEditAppointmentPopup).not.toHaveBeenCalled(); + expect(showTooltipForAppointment).not.toHaveBeenCalled(); + expect(showTooltipForCollector).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts index a5ba370c42f0..484b6f8c88a9 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts @@ -1,7 +1,7 @@ import registerComponent from '@js/core/component_registrator'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; -import type { DxEvent } from '@js/events'; +import type { Cancelable, DxEvent } from '@js/events'; import type { AppointmentClickEvent, AppointmentDblClickEvent, @@ -16,6 +16,7 @@ import type { DOMComponentProperties } from '@ts/core/widget/dom_component'; import DOMComponent from '@ts/core/widget/dom_component'; import type { OptionChanged } from '@ts/core/widget/types'; +import type { AppointmentTooltipExtraOptions } from '../tooltip_strategies/tooltip_strategy_base'; import type { AppointmentTooltipItem, SafeAppointment, ScrollToOptions, TargetedAppointment, ViewType, @@ -67,16 +68,15 @@ export interface AppointmentsProperties extends DOMComponentProperties boolean; scrollTo: (date: Date, options?: ScrollToOptions) => void; - showAppointmentTooltip: ( + showTooltipForAppointment: ( appointment: SafeAppointment, $element: dxElementWrapper, targetedAppointment?: SafeAppointment, ) => void; - showAppointmentTooltipCore: ( + showTooltipForCollector: ( target: dxElementWrapper, data: AppointmentTooltipItem[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options?: any, + options?: AppointmentTooltipExtraOptions, ) => void; showEditAppointmentPopup: ( appointmentData: SafeAppointment, @@ -89,8 +89,6 @@ export class Appointments extends DOMComponent = {}; private viewItems: ViewItem[] = []; @@ -443,22 +441,25 @@ export class Appointments extends DOMComponent { - if (isElementInDom($target) && !this.preventSingleAppointmentClick) { - this.option().showAppointmentTooltip( + this.appointmentClickTimeout = null; + + if (isElementInDom($target)) { + this.option().showTooltipForAppointment( appointmentView.appointmentData, $target, appointmentView.targetedAppointmentData, ); } - - this.preventSingleAppointmentClick = false; }, SHOW_TOOLTIP_TIMEOUT); } @@ -473,19 +474,18 @@ export class Appointments extends DOMComponent this.isVirtualScrolling(), scrollTo: this.scrollTo.bind(this), - showAppointmentTooltip: this.showAppointmentTooltip.bind(this), - showAppointmentTooltipCore: this.showAppointmentTooltipCore.bind(this), + showTooltipForAppointment: this.showAppointmentTooltip.bind(this), + showTooltipForCollector: this.showAppointmentTooltipCore.bind(this), showEditAppointmentPopup: ( appointmentData: SafeAppointment, targetedAppointmentData: TargetedAppointment, @@ -2116,7 +2117,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { } } - showAppointmentTooltipCore(target: dxElementWrapper, data: AppointmentTooltipItem[], options?: any) { + showAppointmentTooltipCore( + target: dxElementWrapper, + data: AppointmentTooltipItem[], + options?: AppointmentTooltipExtraOptions, + ) { const arg: Omit = { cancel: false, appointments: data.map((item) => ({ diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts index bcde9bed148d..0552dc7cd944 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts @@ -67,7 +67,7 @@ interface AppointmentTooltipOptions { onAppointmentClick: (e: AppointmentClickEvent) => void; } -interface AppointmentTooltipExtraOptions { +export interface AppointmentTooltipExtraOptions { clickEvent?: (e: ItemClickEvent) => void; dragBehavior?: (e: ContentReadyEvent) => void; editing?: SchedulerProperties['editing']; From 1d06b3ebf2f7b3741ec11560852d50001639592b Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Thu, 21 May 2026 20:14:38 +0800 Subject: [PATCH 5/5] apply copilot's review --- .../__internal/scheduler/appointments_new/appointments.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts index cd89a9e3cebe..a188e935e645 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts @@ -1002,7 +1002,7 @@ describe('Appointments', () => { element.click(); - expect(onAppointmentClick).not.toHaveBeenCalledTimes(1); + expect(onAppointmentClick).not.toHaveBeenCalled(); }); it('should prevent tooltip showing when onAppointmentClick callback sets e.cancel = true', () => {