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..945cab639f4a 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts @@ -4,6 +4,7 @@ import { import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { Properties } from '@js/ui/scheduler'; +import { fireEvent } from '@testing-library/dom'; import { createScheduler as baseCreateScheduler } from './__mock__/create_scheduler'; import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler'; @@ -375,4 +376,88 @@ describe('New Appointments', () => { expect(onAppointmentRendered).toHaveBeenCalledTimes(1); }); }); + + describe('Keyboard navigation', () => { + it('should delete appointment by delete key', async () => { + const { POM } = await createScheduler({ + dataSource: [{ + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentDate: new Date(2015, 1, 9, 8), + }); + + const appointment = POM.getAppointments()[0]; + appointment.element.focus(); + fireEvent.keyDown(appointment.element, { key: 'Delete' }); + await new Promise(process.nextTick); + + expect(POM.getAppointments().length).toBe(0); + }); + + it('should delete recurring appointment occurrence by delete key', async () => { + const { POM } = await createScheduler({ + dataSource: [{ + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + recurrenceRule: 'FREQ=DAILY;COUNT=3', + }], + currentDate: new Date(2015, 1, 9), + currentView: 'week', + recurrenceEditMode: 'occurrence', + }); + + expect(POM.getAppointments().length).toBe(3); + + const appointment = POM.getAppointments()[0]; + appointment.element.focus(); + fireEvent.keyDown(appointment.element, { key: 'Delete' }); + await new Promise(process.nextTick); + + expect(POM.getAppointments().length).toBe(2); + }); + + it.each([ + { editing: true }, + { editing: { allowDeleting: true } }, + { editing: { allowDeleting: true, allowUpdating: false } }, + ])('should delete appointment when editing=$editing', async ({ editing }) => { + const { POM } = await createScheduler({ + dataSource: [{ + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentDate: new Date(2015, 1, 9, 8), + editing, + }); + + const appointment = POM.getAppointments()[0]; + appointment.element.focus(); + fireEvent.keyDown(appointment.element, { key: 'Delete' }); + await new Promise(process.nextTick); + + expect(POM.getAppointments().length).toBe(0); + }); + + it.each([ + { editing: { allowDeleting: false } }, + { editing: false }, + ])('should NOT delete appointment when editing=$editing', async ({ editing }) => { + const { POM } = await createScheduler({ + dataSource: [{ + startDate: new Date(2015, 1, 9, 8), + endDate: new Date(2015, 1, 9, 9), + }], + currentDate: new Date(2015, 1, 9, 8), + editing, + }); + + const appointment = POM.getAppointments()[0]; + appointment.element.focus(); + fireEvent.keyDown(appointment.element, { key: 'Delete' }); + await new Promise(process.nextTick); + + expect(POM.getAppointments().length).toBe(1); + }); + }); }); 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..65bc1b846b30 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 @@ -36,11 +36,11 @@ export interface BaseAppointmentViewProperties export class BaseAppointmentView< TProperties extends BaseAppointmentViewProperties = BaseAppointmentViewProperties, > extends ViewItem { - protected get targetedAppointmentData(): TargetedAppointment { + public get targetedAppointmentData(): TargetedAppointment { return this.option().targetedAppointmentData; } - protected get appointmentData(): SafeAppointment { + public get appointmentData(): SafeAppointment { return this.option().appointmentData; } diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.focus_controller.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.focus_controller.ts index fafa90451a22..0388b4a13e71 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.focus_controller.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.focus_controller.ts @@ -5,6 +5,8 @@ import { focus } from '@ts/events/m_short'; import { getRawAppointmentGroupValues } from '../utils/resource_manager/appointment_groups_utils'; import type { SortedEntity } from '../view_model/types'; +import type { BaseAppointmentView } from './appointment/base_appointment'; +import { AppointmentCollector } from './appointment_collector'; import type { Appointments } from './appointments'; import type { ViewItem } from './view_item'; @@ -14,7 +16,7 @@ export class AppointmentsFocusController { private needRestoreFocusIndex = -1; private get sortedAppointments(): SortedEntity[] { - return this.appointments.option().getSortedAppointments(); + return this.appointments.option().getSortedItems(); } private get isVirtualScrolling(): boolean { @@ -50,8 +52,27 @@ export class AppointmentsFocusController { } public onViewItemKeyDown(viewItem: ViewItem, e: KeyboardKeyDownEvent): void { - if (e.key === 'Tab') { - this.handleTabKeyDown(e, viewItem.option().sortedIndex); + switch (true) { + case e.key === 'Tab': + this.handleTabKeyDown(e, viewItem.option().sortedIndex); + break; + case e.key === 'Delete': + this.handleDeleteKeyDown(viewItem); + break; + case e.key === 'Home': + this.handleHomeKeyDown(e); + break; + case e.key === 'End': + this.handleEndKeyDown(e); + break; + case e.key === 'Enter': + this.handleEnterKeyDown(viewItem, e); + break; + case e.key === ' ': + this.handleEnterKeyDown(viewItem, e); + break; + default: + break; } } @@ -89,20 +110,64 @@ export class AppointmentsFocusController { } e.originalEvent.preventDefault(); - this.focusByItemData(nextItemData); + this.focusBySortedItem(nextItemData); } - private focusByItemData(itemData: SortedEntity): void { + private handleDeleteKeyDown(viewItem: ViewItem): void { + if (viewItem instanceof AppointmentCollector) { return; } + + const { allowDelete, onDeleteKeyPress } = this.appointments.option(); + if (!allowDelete) { return; } + + const sortedItem = this.sortedAppointments[viewItem.option().sortedIndex]; + if (!sortedItem) { return; } + + const appointmentViewItem = viewItem as BaseAppointmentView; + onDeleteKeyPress({ + appointmentData: sortedItem.itemData, + targetedAppointmentData: appointmentViewItem.targetedAppointmentData, + }); + } + + private handleHomeKeyDown(e: KeyboardKeyDownEvent): void { + const firstSortedItem = this.sortedAppointments[0]; + if (firstSortedItem) { + e.originalEvent.preventDefault(); + this.focusBySortedItem(firstSortedItem); + } + } + + private handleEndKeyDown(e: KeyboardKeyDownEvent): void { + const lastSortedItem = this.sortedAppointments[this.sortedAppointments.length - 1]; + if (lastSortedItem) { + e.originalEvent.preventDefault(); + this.focusBySortedItem(lastSortedItem); + } + } + + private handleEnterKeyDown(viewItem: ViewItem, e: KeyboardKeyDownEvent): void { + const { onItemActivate } = this.appointments.option(); + const sortedItem = this.sortedAppointments[viewItem.option().sortedIndex]; + if (!sortedItem) { return; } + e.originalEvent.preventDefault(); + const appointmentViewItem = viewItem as BaseAppointmentView; + onItemActivate({ + data: sortedItem.itemData, + targetedAppointmentData: appointmentViewItem.targetedAppointmentData, + }); + } + + private focusBySortedItem(sortedItem: SortedEntity): void { if (this.isVirtualScrolling) { - this.scrollToItem(itemData); + this.scrollToItem(sortedItem); } - const viewItem = this.appointments.getViewItemBySortedIndex(itemData.sortedIndex); + const viewItem = this.appointments.getViewItemBySortedIndex(sortedItem.sortedIndex); if (viewItem) { this.focusViewItem(viewItem); } else if (this.isVirtualScrolling) { - this.needRestoreFocusIndex = itemData.sortedIndex; + this.needRestoreFocusIndex = sortedItem.sortedIndex; } } @@ -111,19 +176,19 @@ export class AppointmentsFocusController { focus.trigger(viewItem?.$element()); } - private scrollToItem(itemData: SortedEntity): void { + private scrollToItem(sortedItem: SortedEntity): void { const { getStartViewDate, getResourceManager, scrollTo } = this.appointments.option(); const date = new Date(Math.max( getStartViewDate().getTime(), - itemData.source.startDate, + sortedItem.source.startDate, )); const group = getRawAppointmentGroupValues( - itemData.itemData, + sortedItem.itemData, getResourceManager().resources, ); - scrollTo(date, { group, allDay: itemData.allDay }); + scrollTo(date, { group, allDay: sortedItem.allDay }); } } 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..2e599f214f0b 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts @@ -2,8 +2,8 @@ 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'; import { getResourceManagerMock } from '../__mock__/resource_manager.mock'; import type { ResourceConfig } from '../utils/loader/types'; @@ -41,13 +41,17 @@ const getProperties = (options: { onAppointmentRendered: (): void => {}, getStartViewDate: () => new Date(2024, 0, 1), - getSortedAppointments: () => [], + getSortedItems: () => [], isVirtualScrolling: () => false, scrollTo: (): void => {}, getAppointmentDataSource: mockAppointmentDataSource, getResourceManager: () => getResourceManagerMock(options.resources ?? []), getDataAccessor: () => mockAppointmentDataAccessor, + + allowDelete: false, + onDeleteKeyPress: (): void => {}, + onItemActivate: (): void => {}, }); const createAppointments = ( @@ -67,8 +71,6 @@ const defaultAppointmentData = { describe('Appointments', () => { beforeEach(() => { - fx.off = true; - const $container = $('
') .addClass('container') .appendTo(document.body); @@ -84,8 +86,6 @@ describe('Appointments', () => { afterEach(() => { $('.container').remove(); - fx.off = false; - jest.useRealTimers(); }); describe('Classes', () => { @@ -540,7 +540,7 @@ describe('Appointments', () => { const instance = createAppointments({ ...getProperties(), - getSortedAppointments: () => viewModel as unknown as SortedEntity[], + getSortedItems: () => viewModel as unknown as SortedEntity[], }); instance.option('viewModel', viewModel); @@ -548,9 +548,7 @@ describe('Appointments', () => { const viewItem1 = instance.getViewItemBySortedIndex(1); (viewItem0?.$element().get(0) as HTMLElement).click(); - viewItem0?.$element().get(0).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }), - ); + fireEvent.keyDown(viewItem0?.$element().get(0) as HTMLElement, { key: 'Tab' }); expect(viewItem0?.$element().attr('tabindex')).toBe('-1'); expect(viewItem1?.$element().attr('tabindex')).toBe('0'); @@ -566,7 +564,7 @@ describe('Appointments', () => { const instance = createAppointments({ ...getProperties(), - getSortedAppointments: () => viewModel as unknown as SortedEntity[], + getSortedItems: () => viewModel as unknown as SortedEntity[], }); instance.option('viewModel', viewModel); @@ -574,9 +572,7 @@ describe('Appointments', () => { const viewItem1 = instance.getViewItemBySortedIndex(1); (viewItem1?.$element().get(0) as HTMLElement).click(); - viewItem1?.$element().get(0).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true, bubbles: true }), - ); + fireEvent.keyDown(viewItem1?.$element().get(0) as HTMLElement, { key: 'Tab', shiftKey: true }); expect(viewItem0?.$element().attr('tabindex')).toBe('0'); expect(viewItem1?.$element().attr('tabindex')).toBe('-1'); @@ -637,7 +633,7 @@ describe('Appointments', () => { ...getProperties(), isVirtualScrolling: () => true, scrollTo, - getSortedAppointments: () => [ + getSortedItems: () => [ makeSortedEntity(0), makeSortedEntity(1), makeSortedEntity(1), ], }); @@ -648,9 +644,7 @@ describe('Appointments', () => { const viewItem1 = instance.getViewItemBySortedIndex(0); (viewItem1?.$element().get(0) as HTMLElement).click(); - viewItem1?.$element().get(0).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }), - ); + fireEvent.keyDown(viewItem1?.$element().get(0) as HTMLElement, { key: 'Tab' }); expect(scrollTo).toHaveBeenCalled(); }); @@ -659,7 +653,7 @@ describe('Appointments', () => { const instance = createAppointments({ ...getProperties(), isVirtualScrolling: () => true, - getSortedAppointments: () => [ + getSortedItems: () => [ makeSortedEntity(0), makeSortedEntity(1), makeSortedEntity(2), ], }); @@ -673,9 +667,7 @@ describe('Appointments', () => { const viewItem2 = instance.getViewItemBySortedIndex(2); (viewItem1?.$element().get(0) as HTMLElement).click(); - viewItem1?.$element().get(0).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }), - ); + fireEvent.keyDown(viewItem1?.$element().get(0) as HTMLElement, { key: 'Tab' }); expect(document.activeElement).toBe(viewItem2?.$element().get(0)); }); @@ -688,7 +680,7 @@ describe('Appointments', () => { const instance = createAppointments({ ...getProperties(), isVirtualScrolling: () => true, - getSortedAppointments: () => [ + getSortedItems: () => [ makeSortedEntity(0), makeSortedEntity(1), makeSortedEntity(2), ], }); @@ -698,9 +690,7 @@ describe('Appointments', () => { const viewItem1 = instance.getViewItemBySortedIndex(1); (viewItem1?.$element().get(0) as HTMLElement).click(); - viewItem1?.$element().get(0).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }), - ); + fireEvent.keyDown(viewItem1?.$element().get(0) as HTMLElement, { key: 'Tab' }); // item2 is not rendered yet, so focus cannot move yet expect(instance.getViewItemBySortedIndex(2)).toBeUndefined(); @@ -724,7 +714,7 @@ describe('Appointments', () => { isVirtualScrolling: () => true, scrollTo, getStartViewDate: () => startViewDate, - getSortedAppointments: () => [ + getSortedItems: () => [ makeSortedEntity(0), makeSortedEntity(1, appointmentStartDate), ], }); @@ -734,9 +724,7 @@ describe('Appointments', () => { const viewItem0 = instance.getViewItemBySortedIndex(0); (viewItem0?.$element().get(0) as HTMLElement).click(); - viewItem0?.$element().get(0).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }), - ); + fireEvent.keyDown(viewItem0?.$element().get(0) as HTMLElement, { key: 'Tab' }); expect(scrollTo).toHaveBeenCalledWith(appointmentStartDate, expect.anything()); }); @@ -754,15 +742,13 @@ describe('Appointments', () => { isVirtualScrolling: () => true, scrollTo, getStartViewDate: () => startViewDate, - getSortedAppointments: () => sortedEntities, + getSortedItems: () => sortedEntities, }); instance.option('viewModel', viewModel); const viewItem0 = instance.getViewItemBySortedIndex(0); (viewItem0?.$element().get(0) as HTMLElement).click(); - viewItem0?.$element().get(0).dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }), - ); + fireEvent.keyDown(viewItem0?.$element().get(0) as HTMLElement, { key: 'Tab' }); expect(scrollTo).toHaveBeenCalledWith(startViewDate, expect.anything()); }); @@ -771,9 +757,7 @@ describe('Appointments', () => { describe('Navigation after partial render', () => { const pressTab = (): void => { const activeElement = document.activeElement as HTMLElement; - activeElement.dispatchEvent( - new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }), - ); + fireEvent.keyDown(activeElement, { key: 'Tab' }); }; it('should navigate to the last appointment correctly after an appointment is added', () => { @@ -789,7 +773,7 @@ describe('Appointments', () => { const instance = createAppointments({ ...getProperties(), - getSortedAppointments: () => viewModel as unknown as SortedEntity[], + getSortedItems: () => viewModel as unknown as SortedEntity[], }); instance.option('viewModel', viewModel); @@ -825,7 +809,7 @@ describe('Appointments', () => { const instance = createAppointments({ ...getProperties(), - getSortedAppointments: () => viewModel as unknown as SortedEntity[], + getSortedItems: () => viewModel as unknown as SortedEntity[], }); instance.option('viewModel', viewModel); @@ -857,7 +841,7 @@ describe('Appointments', () => { const instance = createAppointments({ ...getProperties(), - getSortedAppointments: () => viewModel as unknown as SortedEntity[], + getSortedItems: () => viewModel as unknown as SortedEntity[], }); instance.option('viewModel', viewModel); @@ -878,6 +862,220 @@ describe('Appointments', () => { expect(lastViewItem?.$element().attr('tabindex')).toBe('0'); }); }); + + describe('Home/End navigation', () => { + it('should move focus to first appointment on Home key', () => { + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 1 }), + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 2 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem0 = instance.getViewItemBySortedIndex(0); + const viewItem2 = instance.getViewItemBySortedIndex(2); + + (viewItem2?.$element().get(0) as HTMLElement).click(); + fireEvent.keyDown(viewItem2?.$element().get(0) as HTMLElement, { key: 'Home' }); + + expect(viewItem0?.$element().attr('tabindex')).toBe('0'); + expect(viewItem2?.$element().attr('tabindex')).toBe('-1'); + expect(document.activeElement).toBe(viewItem0?.$element().get(0)); + }); + + it('should prevent default browser behavior on Home key', () => { + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 1 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem1 = instance.getViewItemBySortedIndex(1); + (viewItem1?.$element().get(0) as HTMLElement).click(); + + const wasDefaultPrevented = !fireEvent.keyDown( + viewItem1?.$element().get(0) as HTMLElement, + { key: 'Home' }, + ); + + expect(wasDefaultPrevented).toBe(true); + }); + + it('should prevent default browser behavior on End key', () => { + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 1 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem0 = instance.getViewItemBySortedIndex(0); + (viewItem0?.$element().get(0) as HTMLElement).click(); + + const wasDefaultPrevented = !fireEvent.keyDown( + viewItem0?.$element().get(0) as HTMLElement, + { key: 'End' }, + ); + + expect(wasDefaultPrevented).toBe(true); + }); + + it('should move focus to last appointment on End key', () => { + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 1 }), + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 2 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem0 = instance.getViewItemBySortedIndex(0); + const viewItem2 = instance.getViewItemBySortedIndex(2); + + (viewItem0?.$element().get(0) as HTMLElement).click(); + fireEvent.keyDown(viewItem0?.$element().get(0) as HTMLElement, { key: 'End' }); + + expect(viewItem2?.$element().attr('tabindex')).toBe('0'); + expect(viewItem0?.$element().attr('tabindex')).toBe('-1'); + expect(document.activeElement).toBe(viewItem2?.$element().get(0)); + }); + }); + + describe('Keyboard actions', () => { + it('should call onDeleteKeyPress when Delete is pressed and allowDelete is true', () => { + const onDeleteKeyPress = jest.fn(); + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 1 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + allowDelete: true, + onDeleteKeyPress, + getSortedItems: () => [{ + sortedIndex: 0, + itemData: defaultAppointmentData, + source: { startDate: 0 }, + }] as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem = instance.getViewItemBySortedIndex(0); + (viewItem?.$element().get(0) as HTMLElement).click(); + fireEvent.keyDown(viewItem?.$element().get(0) as HTMLElement, { key: 'Delete' }); + + expect(onDeleteKeyPress).toHaveBeenCalledTimes(1); + expect(onDeleteKeyPress).toHaveBeenCalledWith( + expect.objectContaining({ appointmentData: defaultAppointmentData }), + ); + }); + + it('should not call onDeleteKeyPress when Delete is pressed and allowDelete is false', () => { + const onDeleteKeyPress = jest.fn(); + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + allowDelete: false, + onDeleteKeyPress, + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem = instance.getViewItemBySortedIndex(0); + (viewItem?.$element().get(0) as HTMLElement).click(); + fireEvent.keyDown(viewItem?.$element().get(0) as HTMLElement, { key: 'Delete' }); + + expect(onDeleteKeyPress).not.toHaveBeenCalled(); + }); + + it('should not call onDeleteKeyPress when Delete is pressed on appointment collector', () => { + const onDeleteKeyPress = jest.fn(); + const viewModel = [ + mockAppointmentCollectorViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + allowDelete: true, + onDeleteKeyPress, + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem = instance.getViewItemBySortedIndex(0); + (viewItem?.$element().get(0) as HTMLElement).click(); + fireEvent.keyDown(viewItem?.$element().get(0) as HTMLElement, { key: 'Delete' }); + + expect(onDeleteKeyPress).not.toHaveBeenCalled(); + }); + + it('should call onItemActivate when Enter is pressed', () => { + const onItemActivate = jest.fn(); + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + onItemActivate, + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem = instance.getViewItemBySortedIndex(0); + (viewItem?.$element().get(0) as HTMLElement).click(); + fireEvent.keyDown(viewItem?.$element().get(0) as HTMLElement, { key: 'Enter' }); + + expect(onItemActivate).toHaveBeenCalledTimes(1); + expect(onItemActivate).toHaveBeenCalledWith( + expect.objectContaining({ data: defaultAppointmentData }), + ); + }); + it('should call onItemActivate when Space is pressed', () => { + const onItemActivate = jest.fn(); + const viewModel = [ + mockGridViewModel({ ...defaultAppointmentData }, { sortedIndex: 0 }), + ]; + + const instance = createAppointments({ + ...getProperties(), + onItemActivate, + getSortedItems: () => viewModel as unknown as SortedEntity[], + }); + instance.option('viewModel', viewModel); + + const viewItem = instance.getViewItemBySortedIndex(0); + (viewItem?.$element().get(0) as HTMLElement).click(); + fireEvent.keyDown(viewItem?.$element().get(0) as HTMLElement, { key: ' ' }); + + expect(onItemActivate).toHaveBeenCalledTimes(1); + expect(onItemActivate).toHaveBeenCalledWith( + expect.objectContaining({ data: defaultAppointmentData }), + ); + }); + }); }); describe('onAppointmentRendered', () => { diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts index 263b4f4d9fc6..ca64c8e19ede 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts @@ -50,9 +50,17 @@ export interface AppointmentsProperties extends DOMComponentProperties ResourceManager; getDataAccessor: () => AppointmentDataAccessor; getStartViewDate: () => Date; - getSortedAppointments: () => SortedEntity[]; + getSortedItems: () => SortedEntity[]; isVirtualScrolling: () => boolean; scrollTo: (date: Date, options?: ScrollToOptions) => void; + + allowDelete: boolean; + onDeleteKeyPress: (options: + { appointmentData: SafeAppointment; targetedAppointmentData: TargetedAppointment }) => void; + onItemActivate: (options: { + data: SafeAppointment; + targetedAppointmentData: TargetedAppointment; + }) => void; } export class Appointments extends DOMComponent { @@ -109,6 +117,9 @@ export class Appointments extends DOMComponent {}, + allowDelete: false, + onDeleteKeyPress: (): void => {}, + onItemActivate: (): void => {}, }; } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 8289fa26eac5..0efdbb4ad39f 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -503,7 +503,11 @@ class Scheduler extends SchedulerOptionsBaseWidget { this.initEditing(); const { editing } = this; - this.bringEditingModeToAppointments(editing); + if (this.option('_newAppointments')) { + this._appointments.option('allowDelete', this.editing.allowDeleting); + } else { + this.bringEditingModeToAppointments(editing); + } this.hideAppointmentTooltip(); this.createAppointmentPopupForm(); @@ -1071,6 +1075,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const appointmentsConfig: Partial = { tabIndex: this.option('tabIndex'), currentView: this.option('currentView') as ViewType, + allowDelete: this.editing.allowDeleting, appointmentTemplate: this.getViewOption('appointmentTemplate'), appointmentCollectorTemplate: this.getViewOption('appointmentCollectorTemplate'), onAppointmentRendered: (e) => { @@ -1081,11 +1086,18 @@ class Scheduler extends SchedulerOptionsBaseWidget { targetedAppointmentData: e.targetedAppointmentData, }); }, + onDeleteKeyPress: (e) => { + this.checkAndDeleteAppointment(e.appointmentData, e.targetedAppointmentData); + }, + onItemActivate: ({ data, targetedAppointmentData }) => { + this.showAppointmentPopup(data, undefined, targetedAppointmentData); + }, + getResourceManager: () => this.resourceManager, getAppointmentDataSource: () => this.appointmentDataSource, getDataAccessor: () => this._dataAccessors, getStartViewDate: () => this.getStartViewDate(), - getSortedAppointments: () => this._layoutManager.sortedItems, + getSortedItems: () => this._layoutManager.sortedItems, isVirtualScrolling: () => this.isVirtualScrolling(), scrollTo: this.scrollTo.bind(this), @@ -1231,7 +1243,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { targetedAppointment, this._dataAccessors, ); - const deletingOptions = this.fireOnAppointmentDeleting(appointment, targetedAdapter); this.checkRecurringAppointment( appointment,