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 c8ae39a7a594..b424f2d8f973 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/model/scheduler.ts @@ -132,6 +132,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..ba81f77f6b1c 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,370 @@ 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 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 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(); + + 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 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) }, + { 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 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); + }); + }); + + 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 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 9030a3d537ae..c6b6acb7a187 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -1057,7 +1057,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..8fb0e262b724 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 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'; 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,6 +28,7 @@ export interface AppointmentCollectorProperties }; targetedAppointmentData: TargetedAppointment; appointmentCollectorTemplate: TemplateBase; + onClick: (viewItem: AppointmentCollector, e: DxEvent) => void; } export class AppointmentCollector @@ -34,7 +38,20 @@ export class AppointmentCollector private buttonInstance?: Button; private get appointmentsCount(): number { - return this.option().appointmentsData.length; + return this.option().items.length; + } + + override _setOptionsByReference(): void { + super._setOptionsByReference(); + + // Note: items have appointmentData, which is used as a key in dataSource + this._optionsByReference = { + ...this._optionsByReference, + // @ts-expect-error Component class has wrong type for _optionsByReference + items: true, + // @ts-expect-error Component class has wrong type for _optionsByReference + targetedAppointmentData: true, + }; } override _init(): void { @@ -115,10 +132,12 @@ export class AppointmentCollector model: { appointmentCount: this.appointmentsCount, isCompact: this.option().isCompact, - items: this.option().appointmentsData, + items: this.option().items.map((item) => item.itemData), }, })), - 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..a188e935e645 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'; @@ -39,6 +40,8 @@ const getProperties = (options: { appointmentCollectorTemplate: 'appointmentCollector', onAppointmentRendered: (): void => {}, + onAppointmentClick: (): void => {}, + onAppointmentDblClick: (): void => {}, getStartViewDate: () => new Date(2024, 0, 1), getSortedAppointments: () => [], @@ -48,6 +51,10 @@ const getProperties = (options: { getAppointmentDataSource: mockAppointmentDataSource, getResourceManager: () => getResourceManagerMock(options.resources ?? []), getDataAccessor: () => mockAppointmentDataAccessor, + + showTooltipForAppointment: (): void => {}, + showTooltipForCollector: (): void => {}, + showEditAppointmentPopup: (): void => {}, }); const createAppointments = ( @@ -65,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; @@ -891,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, }), }), ); @@ -912,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, }), }), ); @@ -966,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.toHaveBeenCalled(); + }); + + 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 263b4f4d9fc6..f9f2ed3dd111 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts @@ -1,14 +1,24 @@ 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 { Cancelable, 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 { AppointmentTooltipExtraOptions } from '../tooltip_strategies/tooltip_strategy_base'; import type { + AppointmentTooltipItem, SafeAppointment, ScrollToOptions, TargetedAppointment, ViewType, } from '../types'; import type { AppointmentDataAccessor } from '../utils/data_accessor/appointment_data_accessor'; @@ -23,7 +33,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 +44,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 +56,9 @@ export interface AppointmentsProperties extends DOMComponentProperties void; + onAppointmentClick: (e: AppointmentClickEvent) => void; + onAppointmentDblClick: (e: AppointmentDblClickEvent) => void; getAppointmentDataSource: () => AppointmentDataSource; getResourceManager: () => ResourceManager; @@ -52,12 +66,29 @@ export interface AppointmentsProperties extends DOMComponentProperties Date; getSortedAppointments: () => SortedEntity[]; isVirtualScrolling: () => boolean; + scrollTo: (date: Date, options?: ScrollToOptions) => void; + showTooltipForAppointment: ( + appointment: SafeAppointment, + $element: dxElementWrapper, + targetedAppointment?: SafeAppointment, + ) => void; + showTooltipForCollector: ( + target: dxElementWrapper, + data: AppointmentTooltipItem[], + options?: AppointmentTooltipExtraOptions, + ) => void; + showEditAppointmentPopup: ( + appointmentData: SafeAppointment, + targetedAppointmentData: TargetedAppointment, + ) => void; } export class Appointments extends DOMComponent { private focusController!: AppointmentsFocusController; + private appointmentClickTimeout: number | null = null; + private viewItemBySortedIndex: Record = {}; private viewItems: ViewItem[] = []; @@ -94,6 +125,14 @@ export class Appointments extends DOMComponent {}; + return { ...super._getDefaultOptions(), tabIndex: 0, @@ -108,7 +149,9 @@ export class Appointments extends DOMComponent {}, + onAppointmentRendered: noop, + onAppointmentClick: noop, + onAppointmentDblClick: noop, }; } @@ -279,14 +322,13 @@ export class Appointments extends DOMComponent item.itemData), + items: appointmentViewModel.items, isCompact: appointmentViewModel.isCompact, geometry: { height: appointmentViewModel.height, @@ -296,6 +338,7 @@ export class Appointments extends DOMComponent { + this.appointmentClickTimeout = null; + + if (isElementInDom($target)) { + this.option().showTooltipForAppointment( + appointmentView.appointmentData, + $target, + appointmentView.targetedAppointmentData, + ); + } + }, SHOW_TOOLTIP_TIMEOUT); + } + + private onAppointmentDblClick( + appointmentView: BaseAppointmentView, + event: DxEvent, + ): void { + const e = { + appointmentElement: getPublicElement(appointmentView.$element()), + appointmentData: appointmentView.appointmentData, + targetedAppointmentData: appointmentView.targetedAppointmentData, + event, + }; + + if (this.appointmentClickTimeout) { + clearTimeout(this.appointmentClickTimeout); + this.appointmentClickTimeout = null; + } + + // @ts-expect-error 'component' and 'element' are set by action + this.option().onAppointmentDblClick(e); + + if ((e as Cancelable).cancel) { + return; + } + + 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().showTooltipForCollector( + collector.$element(), + collectorTooltipItems, + { + dragBehavior: undefined, // TODO + isButtonClick: true, + // eslint-disable-next-line @typescript-eslint/naming-convention + _loopFocus: 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_compact_appointments_helper.ts b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts index c0ea0ffc3a0c..536682d86474 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -62,6 +62,7 @@ export class CompactAppointmentsHelper { ? this.createTooltipDragBehavior($appointmentCollector).bind(this) : undefined, isButtonClick: true, + // eslint-disable-next-line @typescript-eslint/naming-convention _loopFocus: true, }; } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index e092193b3fde..d1970d0578b8 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, FirstDayOfWeek, - Properties as SchedulerProperties, } from '@js/ui/scheduler'; import errors from '@js/ui/widget/ui.errors'; import { dateUtilsTs } from '@ts/core/utils/date'; @@ -65,6 +64,7 @@ import { validateRRule } from './recurrence/validate_rule'; import { SchedulerOptionsBaseWidget } from './scheduler_options_base_widget'; import { DesktopTooltipStrategy } from './tooltip_strategies/desktop_tooltip_strategy'; import { MobileTooltipStrategy } from './tooltip_strategies/mobile_tooltip_strategy'; +import type { AppointmentTooltipExtraOptions } from './tooltip_strategies/tooltip_strategy_base'; import type { AppointmentTooltipItem, SafeAppointment, @@ -232,8 +232,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { private timeZonesPromise!: Promise; - private appointmentRenderedAction!: SchedulerProperties['onAppointmentRendered']; - get timeZoneCalculator() { if (!this.timeZoneCalculatorInstance) { this.timeZoneCalculatorInstance = createTimeZoneCalculator(this.option('timeZone')); @@ -424,16 +422,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)); @@ -804,12 +810,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() { @@ -997,6 +997,9 @@ class Scheduler extends SchedulerOptionsBaseWidget { onAppointmentDeleted: this._createActionByOption(StoreEventNames.DELETED), onAppointmentFormOpening: this._createActionByOption('onAppointmentFormOpening'), onAppointmentTooltipShowing: this._createActionByOption('onAppointmentTooltipShowing'), + onAppointmentRendered: this._createActionByOption('onAppointmentRendered'), + onAppointmentClick: this._createActionByOption('onAppointmentClick'), + onAppointmentDblClick: this._createActionByOption('onAppointmentDblClick'), }; } @@ -1061,22 +1064,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), + showTooltipForAppointment: this.showAppointmentTooltip.bind(this), + showTooltipForCollector: this.showAppointmentTooltipCore.bind(this), + showEditAppointmentPopup: ( + appointmentData: SafeAppointment, + targetedAppointmentData: TargetedAppointment, + ) => { + this.showAppointmentPopup(appointmentData, false, targetedAppointmentData); + }, }; // @ts-expect-error this._appointments = this._createComponent('
', Appointments, appointmentsConfig); @@ -1197,9 +1205,12 @@ 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), }; } + // TODO: delete this method when old impl is removed _createEventArgs(e) { const config = { itemData: e.itemData.appointment, @@ -2084,7 +2095,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/desktop_tooltip_strategy.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy.ts index 45928ca7dc5b..4e3592896b53 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/desktop_tooltip_strategy.ts @@ -57,7 +57,7 @@ export class DesktopTooltipStrategy extends TooltipStrategyBase { onShown: this.onShown.bind(this), contentTemplate: this.getContentTemplate(dataList), wrapperAttr: { class: APPOINTMENT_TOOLTIP_WRAPPER_CLASS }, - // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/naming-convention _loopFocus: this.extraOptions?._loopFocus, }) as Tooltip; 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 176edd9470cc..b64c4bd80abd 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,9 +63,11 @@ interface AppointmentTooltipOptions { getAppointmentDisabled: (appointment: Appointment) => boolean | undefined; onItemContextMenu: (eventArgs: unknown) => void; createEventArgs: (e: ItemContextMenuEvent) => unknown; + newAppointments?: boolean; // TODO + onAppointmentClick: (e: AppointmentClickEvent) => void; } -interface AppointmentTooltipExtraOptions { +export interface AppointmentTooltipExtraOptions { clickEvent?: (e: ItemClickEvent) => void; dragBehavior?: (e: ContentReadyEvent) => void; editing?: SchedulerProperties['editing']; @@ -73,7 +75,8 @@ interface AppointmentTooltipExtraOptions { isButtonClick?: boolean; offset?: unknown; rtlEnabled?: boolean; - tabFocusLoopEnabled?: boolean; + // eslint-disable-next-line @typescript-eslint/naming-convention + _loopFocus?: boolean; } export abstract class TooltipStrategyBase { @@ -245,6 +248,7 @@ export abstract class TooltipStrategyBase { item.color, ), // @ts-expect-error + // eslint-disable-next-line @typescript-eslint/naming-convention _swipeEnabled: false, pageLoadMode: 'scrollBottom', }; @@ -326,7 +330,21 @@ export abstract class TooltipStrategyBase { } this.hide(); - this.extraOptions?.clickEvent?.(e); + + if (this._options.newAppointments) { + 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); + } + this._options.showAppointmentPopup( e.itemData.appointment, false, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.events.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.events.tests.js index fee559f2b6cf..ba830dd52a93 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.events.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/common.events.tests.js @@ -626,7 +626,10 @@ QUnit.module('Events', { 'onAppointmentDeleting': function() { return true; }, 'onAppointmentDeleted': function() { return true; }, 'onAppointmentFormOpening': function() { return true; }, - 'onAppointmentTooltipShowing': function() { return true; } + 'onAppointmentTooltipShowing': function() { return true; }, + 'onAppointmentRendered': function() { return true; }, + 'onAppointmentClick': function() { return true; }, + 'onAppointmentDblClick': function() { return true; }, }); $.each(scheduler.instance.getActions(), function(name, action) {