Skip to content

Commit 6a45028

Browse files
authored
Scheduler - Refactor Appointments Collection - KBN (#33672)
1 parent 3744012 commit 6a45028

6 files changed

Lines changed: 559 additions & 62 deletions

File tree

packages/devextreme/js/__internal/scheduler/__tests__/appointments_new.test.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
import type { dxElementWrapper } from '@js/core/renderer';
55
import $ from '@js/core/renderer';
66
import type { Properties } from '@js/ui/scheduler';
7+
import { fireEvent } from '@testing-library/dom';
78

89
import { createScheduler as baseCreateScheduler } from './__mock__/create_scheduler';
910
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';
@@ -756,4 +757,88 @@ describe('New Appointments', () => {
756757
expect(appointment.text).toBe('Updated Appointment');
757758
});
758759
});
760+
761+
describe('Keyboard navigation', () => {
762+
it('should delete appointment by delete key', async () => {
763+
const { POM } = await createScheduler({
764+
dataSource: [{
765+
startDate: new Date(2015, 1, 9, 8),
766+
endDate: new Date(2015, 1, 9, 9),
767+
}],
768+
currentDate: new Date(2015, 1, 9, 8),
769+
});
770+
771+
const appointment = POM.getAppointments()[0];
772+
appointment.element.focus();
773+
fireEvent.keyDown(appointment.element, { key: 'Delete' });
774+
await new Promise(process.nextTick);
775+
776+
expect(POM.getAppointments().length).toBe(0);
777+
});
778+
779+
it('should delete recurring appointment occurrence by delete key', async () => {
780+
const { POM } = await createScheduler({
781+
dataSource: [{
782+
startDate: new Date(2015, 1, 9, 8),
783+
endDate: new Date(2015, 1, 9, 9),
784+
recurrenceRule: 'FREQ=DAILY;COUNT=3',
785+
}],
786+
currentDate: new Date(2015, 1, 9),
787+
currentView: 'week',
788+
recurrenceEditMode: 'occurrence',
789+
});
790+
791+
expect(POM.getAppointments().length).toBe(3);
792+
793+
const appointment = POM.getAppointments()[0];
794+
appointment.element.focus();
795+
fireEvent.keyDown(appointment.element, { key: 'Delete' });
796+
await new Promise(process.nextTick);
797+
798+
expect(POM.getAppointments().length).toBe(2);
799+
});
800+
801+
it.each([
802+
{ editing: true },
803+
{ editing: { allowDeleting: true } },
804+
{ editing: { allowDeleting: true, allowUpdating: false } },
805+
])('should delete appointment when editing=$editing', async ({ editing }) => {
806+
const { POM } = await createScheduler({
807+
dataSource: [{
808+
startDate: new Date(2015, 1, 9, 8),
809+
endDate: new Date(2015, 1, 9, 9),
810+
}],
811+
currentDate: new Date(2015, 1, 9, 8),
812+
editing,
813+
});
814+
815+
const appointment = POM.getAppointments()[0];
816+
appointment.element.focus();
817+
fireEvent.keyDown(appointment.element, { key: 'Delete' });
818+
await new Promise(process.nextTick);
819+
820+
expect(POM.getAppointments().length).toBe(0);
821+
});
822+
823+
it.each([
824+
{ editing: { allowDeleting: false } },
825+
{ editing: false },
826+
])('should NOT delete appointment when editing=$editing', async ({ editing }) => {
827+
const { POM } = await createScheduler({
828+
dataSource: [{
829+
startDate: new Date(2015, 1, 9, 8),
830+
endDate: new Date(2015, 1, 9, 9),
831+
}],
832+
currentDate: new Date(2015, 1, 9, 8),
833+
editing,
834+
});
835+
836+
const appointment = POM.getAppointments()[0];
837+
appointment.element.focus();
838+
fireEvent.keyDown(appointment.element, { key: 'Delete' });
839+
await new Promise(process.nextTick);
840+
841+
expect(POM.getAppointments().length).toBe(1);
842+
});
843+
});
759844
});

packages/devextreme/js/__internal/scheduler/appointments_new/appointment/base_appointment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ export interface BaseAppointmentViewProperties
3838
export class BaseAppointmentView<
3939
TProperties extends BaseAppointmentViewProperties = BaseAppointmentViewProperties,
4040
> extends ViewItem<TProperties> {
41-
get targetedAppointmentData(): TargetedAppointment {
41+
public get targetedAppointmentData(): TargetedAppointment {
4242
return this.option().targetedAppointmentData;
4343
}
4444

45-
get appointmentData(): SafeAppointment {
45+
public get appointmentData(): SafeAppointment {
4646
return this.option().appointmentData;
4747
}
4848

packages/devextreme/js/__internal/scheduler/appointments_new/appointments.focus_controller.ts

Lines changed: 85 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@ import { focus } from '@ts/events/m_short';
55

66
import { getRawAppointmentGroupValues } from '../utils/resource_manager/appointment_groups_utils';
77
import type { SortedEntity } from '../view_model/types';
8+
import type { BaseAppointmentView } from './appointment/base_appointment';
9+
import { AppointmentCollector } from './appointment_collector';
810
import type { Appointments } from './appointments';
911
import type { ViewItem } from './view_item';
1012

13+
interface AppointmentsFocusControllerHandlers {
14+
onAppointmentEnterKeyDown: (appointmentView: BaseAppointmentView, event: DxEvent) => void;
15+
}
16+
1117
export class AppointmentsFocusController {
1218
private focusableSortedIndex = 0;
1319

1420
private needRestoreFocusIndex = -1;
1521

16-
private get sortedAppointments(): SortedEntity[] {
17-
return this.appointments.option().getSortedAppointments();
22+
private get sortedItems(): SortedEntity[] {
23+
return this.appointments.option().getSortedItems();
1824
}
1925

2026
private get isVirtualScrolling(): boolean {
@@ -25,7 +31,10 @@ export class AppointmentsFocusController {
2531
return this.appointments.option().tabIndex;
2632
}
2733

28-
constructor(private readonly appointments: Appointments) { }
34+
constructor(
35+
private readonly appointments: Appointments,
36+
private readonly handlers: AppointmentsFocusControllerHandlers,
37+
) { }
2938

3039
public onViewItemClick(viewItem: ViewItem): void {
3140
this.focusViewItem(viewItem);
@@ -50,8 +59,27 @@ export class AppointmentsFocusController {
5059
}
5160

5261
public onViewItemKeyDown(viewItem: ViewItem, e: KeyboardKeyDownEvent): void {
53-
if (e.key === 'Tab') {
54-
this.handleTabKeyDown(e, viewItem.option().sortedIndex);
62+
switch (true) {
63+
case e.key === 'Tab':
64+
this.handleTabKeyDown(e, viewItem.option().sortedIndex);
65+
break;
66+
case e.key === 'Delete':
67+
this.handleDeleteKeyDown(viewItem);
68+
break;
69+
case e.key === 'Home':
70+
this.handleHomeKeyDown(e);
71+
break;
72+
case e.key === 'End':
73+
this.handleEndKeyDown(e);
74+
break;
75+
case e.key === 'Enter':
76+
this.handleEnterKeyDown(viewItem, e);
77+
break;
78+
case e.key === ' ':
79+
this.handleEnterKeyDown(viewItem, e);
80+
break;
81+
default:
82+
break;
5583
}
5684
}
5785

@@ -82,27 +110,69 @@ export class AppointmentsFocusController {
82110

83111
private handleTabKeyDown(e: KeyboardKeyDownEvent, sortedIndex: number): void {
84112
const nextIndex = sortedIndex + (e.shift ? -1 : 1);
85-
const nextItemData = this.sortedAppointments[nextIndex];
113+
const nextItemData = this.sortedItems[nextIndex];
86114

87115
if (!nextItemData) {
88116
return;
89117
}
90118

91119
e.originalEvent.preventDefault();
92-
this.focusByItemData(nextItemData);
120+
this.focusBySortedItem(nextItemData);
121+
}
122+
123+
private handleDeleteKeyDown(viewItem: ViewItem): void {
124+
if (viewItem instanceof AppointmentCollector) { return; }
125+
126+
const { allowDelete, onDeleteKeyPress } = this.appointments.option();
127+
if (!allowDelete) { return; }
128+
129+
const appointmentViewItem = viewItem as BaseAppointmentView;
130+
onDeleteKeyPress({
131+
appointmentData: appointmentViewItem.appointmentData,
132+
targetedAppointmentData: appointmentViewItem.targetedAppointmentData,
133+
});
134+
}
135+
136+
private handleHomeKeyDown(e: KeyboardKeyDownEvent): void {
137+
const firstSortedItem = this.sortedItems[0];
138+
if (firstSortedItem) {
139+
e.originalEvent.preventDefault();
140+
this.focusBySortedItem(firstSortedItem);
141+
}
142+
}
143+
144+
private handleEndKeyDown(e: KeyboardKeyDownEvent): void {
145+
const lastSortedItem = this.sortedItems[this.sortedItems.length - 1];
146+
if (lastSortedItem) {
147+
e.originalEvent.preventDefault();
148+
this.focusBySortedItem(lastSortedItem);
149+
}
150+
}
151+
152+
private handleEnterKeyDown(viewItem: ViewItem, e: KeyboardKeyDownEvent): void {
153+
e.originalEvent.preventDefault();
154+
155+
if (viewItem instanceof AppointmentCollector) {
156+
return;
157+
}
158+
159+
this.handlers.onAppointmentEnterKeyDown(
160+
viewItem as BaseAppointmentView,
161+
e.originalEvent as DxEvent,
162+
);
93163
}
94164

95-
private focusByItemData(itemData: SortedEntity): void {
165+
private focusBySortedItem(sortedItem: SortedEntity): void {
96166
if (this.isVirtualScrolling) {
97-
this.scrollToItem(itemData);
167+
this.scrollToItem(sortedItem);
98168
}
99169

100-
const viewItem = this.appointments.getViewItemBySortedIndex(itemData.sortedIndex);
170+
const viewItem = this.appointments.getViewItemBySortedIndex(sortedItem.sortedIndex);
101171

102172
if (viewItem) {
103173
this.focusViewItem(viewItem);
104174
} else if (this.isVirtualScrolling) {
105-
this.needRestoreFocusIndex = itemData.sortedIndex;
175+
this.needRestoreFocusIndex = sortedItem.sortedIndex;
106176
}
107177
}
108178

@@ -111,19 +181,19 @@ export class AppointmentsFocusController {
111181
focus.trigger(viewItem?.$element());
112182
}
113183

114-
private scrollToItem(itemData: SortedEntity): void {
184+
private scrollToItem(sortedItem: SortedEntity): void {
115185
const { getStartViewDate, getResourceManager, scrollTo } = this.appointments.option();
116186

117187
const date = new Date(Math.max(
118188
getStartViewDate().getTime(),
119-
itemData.source.startDate,
189+
sortedItem.source.startDate,
120190
));
121191

122192
const group = getRawAppointmentGroupValues(
123-
itemData.itemData,
193+
sortedItem.itemData,
124194
getResourceManager().resources,
125195
);
126196

127-
scrollTo(date, { group, allDay: itemData.allDay });
197+
scrollTo(date, { group, allDay: sortedItem.allDay });
128198
}
129199
}

0 commit comments

Comments
 (0)