Scheduler - Refactor Appointments Collection - KBN#33643
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the new Scheduler appointments architecture (appointments_new/) with keyboard navigation/activation and delete-by-key support, wiring these behaviors up from Scheduler into the new Appointments component and adding Jest coverage for the new key interactions.
Changes:
- Added
allowDelete,onDeleteKeyPress, andonItemActivatetoAppointmentsPropertiesand wired them fromScheduler. - Expanded
AppointmentsFocusControllerkey handling to support Delete/Home/End/Enter/Space. - Updated/added Jest tests to use
@testing-library/domkey events and to cover the new keyboard behaviors.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/devextreme/js/__internal/scheduler/m_scheduler.ts | Passes delete/activate callbacks + allowDelete into new Appointments configuration. |
| packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts | Extends AppointmentsProperties and provides default options for new callbacks/flags. |
| packages/devextreme/js/__internal/scheduler/appointments_new/appointments.focus_controller.ts | Implements the new key handling logic (Delete/Home/End/Enter/Space). |
| packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts | Updates keydown simulation and adds unit tests for Home/End and keyboard actions. |
| packages/devextreme/js/__internal/scheduler/tests/appointments_new.test.ts | Adds integration tests for deleting appointments via the Delete key (including recurring occurrences). |
| const appointmentsConfig: Partial<AppointmentsProperties> = { | ||
| tabIndex: this.option('tabIndex'), | ||
| currentView: this.option('currentView') as ViewType, | ||
| allowDelete: this.editing.allowUpdating && this.editing.allowDeleting, |
There was a problem hiding this comment.
allowDelete value is only passed to Appointments on init, but there wasn't added a code that would update allowDelete on runtime change.
But actually deleting works as expected, because the legacy implementation does it for new appointments here.
Even though it works, I think we can improve something there:
-
this repaint call is redundant for the new appointments.
-
Weird calculation for allowDelete. In tooltip to delete appointmen just
editing.allowDeleteis used.
What I suggest to do:
-
pass scheduler.editing.allowDelete value to Appointments. Scheduler normalizes the 'editing' option here. We need to pass this to new Appointments
-
Don't call
bringEditingModeToAppointmentshere for new Appointments. Just update Appointments.option().allowDeleting directly via:Appointments.option('allowDeleting' this.editing.allowDeleting) -
Add integration tests for these cases (here):
a. Appt can be deleted when editing=true
b. Appt can be deleted when editing.allowDeleting=true
c. Appt can be deleted when editing.allowDeleting=true and editing.allowUpdating=false
d. Appt cant be deleted when editing.allowDeleting=false
e. Appt cant be deleted when editing=false
I think it can be a matrix test.
Please let me know if you have any questions
There was a problem hiding this comment.
In case c (allowDeleting=true, allowUpdating=false) - should Delete key work? The tooltip only checks allowDeleting, but allowUpdating=false implies the data is read-only. Should keyboard delete follow the same logic as tooltip, or should it respect allowUpdating too?
There was a problem hiding this comment.
I think we should follow tooltip's behavior (only check allowDeleting), because doc doesn't state that allowUpdating must be enabled too to allow appt deletion
| const { allowDelete, onDeleteKeyPress, getDataAccessor } = this.appointments.option(); | ||
| if (!allowDelete) { return; } | ||
|
|
||
| const entity = this.sortedAppointments[sortedIndex]; |
There was a problem hiding this comment.
Let's avoid variable name entity because it's very generic and tells nothing.
I see that the name of the type is SortedEntity, but it's also something that we can improve.
I suggest to rename entity to itemData, the same applies to all other places where name entity is used
There was a problem hiding this comment.
SortedEntity already has a field called .itemData (the appointment data inside). So if we name the variable itemData, you'd write itemData.itemData to access the appointment.
How about sortedItem instead - consistent with sortedIndex, sortedAppointments, and sortedItems used elsewhere?
There was a problem hiding this comment.
You're right, I agree with sortedItem. Can you please update names of other occurrences of itemData?
focusByItemData -> focusBySortedItem
There was a problem hiding this comment.
What do you think about renaming getSortedAppointments to getSortedItems, so that it would be consistent everywhere?
| if (!allowDelete) { return; } | ||
|
|
||
| const entity = this.sortedAppointments[sortedIndex]; | ||
| if (!entity) { return; } |
There was a problem hiding this comment.
There was a problem hiding this comment.
Confirmed - the guard doesn't protect here because collector sortedIndex may overlap with sortedAppointments indices, potentially deleting the wrong appointment. We'll add an explicit instanceof AppointmentCollector check in onViewItemKeyDown to skip Delete on collectors entirely. This matches your suggestion to prevent deleting in this case. What do you think?
There was a problem hiding this comment.
Yep, that's ok for me. Please also add a test.
And small suggestion: let's move this if-statement inside the handleDeleteKeydown and pass viewItem to all the keydown handlers instead of sortedIndex
| const occurrence = { ...entity.itemData }; | ||
| getDataAccessor().set('startDate', occurrence, new Date(entity.source.startDate)); | ||
|
|
||
| onDeleteKeyPress({ appointment: entity.itemData, occurrence }); |
There was a problem hiding this comment.
This is a strange way to get occurrence data, because there's a dedicated function for that: getTargetedAppointmentData.
I suggest to make BaseAppointment.targetedAppointmentData and BaseAppointment.appointmentData properties to be public, so we could use it here:
onDeleteKeyPress({
appointmentData: viewItem.appointmentData,
targetedAppointmentData: viewItem.targetedAppointmentData
})Actually I have made these props in my PR to be public
|
|
||
| private handleHomeKeyDown(): void { | ||
| const firstAppointment = this.sortedAppointments[0]; | ||
| if (firstAppointment) { this.focusByItemData(firstAppointment); } |
There was a problem hiding this comment.
We need e.preventDefault here, like in the legacy impl.
Otherwise body element may scroll to the bottom if it has large height.
Please add a jest test to check that event is prevented on 'home' press
|
|
||
| private handleEndKeyDown(): void { | ||
| const lastAppointment = this.sortedAppointments[this.sortedAppointments.length - 1]; | ||
| if (lastAppointment) { this.focusByItemData(lastAppointment); } |
There was a problem hiding this comment.
Same as for Home key down: https://github.com/DevExpress/DevExtreme/pull/33643/changes#r3279707087
c517882 to
9c60beb
Compare

What
Keyboard navigation for new
appointments_new/architecture (Del/Home/End/Enter/Space)How
editing.allowDeleting,onDeleteKeyPress,onItemActivateonAppointmentsPropertiesfocus_controller: switch(true) per key inonViewItemKeyDownstartDate, recurring delete excludes right instance