Scheduler - Appointments Refactoring - Support click & double click#33633
Scheduler - Appointments Refactoring - Support click & double click#33633Tucchhaa wants to merge 5 commits into
Conversation
| super._dispose(); | ||
|
|
||
| click.off(this.$element(), EVENTS_NAMESPACE); | ||
| dxClick.off(this.$element(), EVENTS_NAMESPACE); |
There was a problem hiding this comment.
old impl also uses dxClick event
There was a problem hiding this comment.
Pull request overview
Refactors the new scheduler appointments architecture to support onAppointmentClick and onAppointmentDblClick events (previously only onAppointmentRendered was wired up), and integrates tooltip-on-click and edit-popup-on-double-click flows. Click handling is moved from ViewItem/focusController to dedicated handlers in the Appointments component, and the tooltip strategy is updated to dispatch the new onAppointmentClick event when _newAppointments is on. Tests are added covering click/dblclick callbacks, tooltip and popup flows, and recurrence dialog.
Changes:
- Add
onAppointmentClick/onAppointmentDblClick/onAppointmentRenderedplumbing throughAppointments,BaseAppointmentView,AppointmentCollector, andTooltipStrategyBase, including dxclick/dxdblclick wiring and tooltip-suppression timeout logic. - Refactor scheduler option handling: actions stored in
this.actions, removingappointmentRenderedAction/createAppointmentRenderedAction; collector now consumes view models (items) instead of rawappointmentsData. - Add comprehensive Jest tests for click, double click, tooltip showing, and popup flows in new appointments.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/devextreme/js/__internal/scheduler/tooltip_strategies/tooltip_strategy_base.ts | Dispatches onAppointmentClick for new appointments mode on tooltip item click. |
| packages/devextreme/js/__internal/scheduler/m_scheduler.ts | Routes click/dblclick/rendered actions through this.actions; removes legacy appointmentRenderedAction; passes new wiring into Appointments and tooltip options. |
| packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts | Drops unused groups from appointment config. |
| packages/devextreme/js/__internal/scheduler/appointments_new/view_item.ts | Removes built-in onClick from ViewItem (now handled in Appointments). |
| packages/devextreme/js/__internal/scheduler/appointments_new/view_item.test.ts | Updates test props to match removed onClick. |
| packages/devextreme/js/__internal/scheduler/appointments_new/appointments.ts | Adds click/dblclick handlers, tooltip-show timeout, collector click flow, and new option types. |
| packages/devextreme/js/__internal/scheduler/appointments_new/appointments.test.ts | Updates default props with new callbacks/showers. |
| packages/devextreme/js/__internal/scheduler/appointments_new/appointment/base_appointment.ts | Switches to dxClick/dxdblclick events; exposes appointment data getters; wires onClick/onDblClick to parent. |
| packages/devextreme/js/__internal/scheduler/appointments_new/appointment_collector.ts | Replaces appointmentsData option with items view models; adds external onClick. |
| packages/devextreme/js/__internal/scheduler/appointments_new/mock/base_appointment_view.ts | Adds onDblClick mock prop. |
| packages/devextreme/js/__internal/scheduler/appointments_new/mock/appointment_collector.ts | Maps mocks to new items prop. |
| packages/devextreme/js/__internal/scheduler/tests/appointments_new.test.ts | New tests for click, dblclick, tooltip, popup, and recurrence dialog flows. |
| packages/devextreme/js/__internal/scheduler/tests/mock/model/scheduler.ts | Adds isRecurrenceDialogVisible helper. |
| { | ||
| dragBehavior: undefined, // TODO | ||
| isButtonClick: true, | ||
| tabFocusLoopEnabled: true, | ||
| }, |
There was a problem hiding this comment.
old implementation also passed clickEventproperty (here).
clickEvent is used in tooltip to call onAppointmentClick handler when tooltip item is clicked.
In new implementation, onAppointmentClick is directly used in the tooltip.
There was a problem hiding this comment.
P.S. when onAppointmentClick is called on the tooltip item click, setting e.cancel=true has no effect.
I think we might need to discuss it with PMs. I would suggest to make e.cancel prevent appointment popup from showing if appointment is inside the tooltip. However in this case, we would need to let user know if the clicked appointment is inside tooltip or not.
I would do that after the refactoring is done, because it's not urgent
| // setTimeout is used to prevent showing tooltip on double click | ||
| this.appointmentClickTimeout = window.setTimeout(() => { | ||
| if (isElementInDom($target) && !this.preventSingleAppointmentClick) { | ||
| this.option().showAppointmentTooltip( | ||
| appointmentView.appointmentData, | ||
| $target, | ||
| appointmentView.targetedAppointmentData, | ||
| ); | ||
| } | ||
|
|
||
| this.preventSingleAppointmentClick = false; | ||
| }, SHOW_TOOLTIP_TIMEOUT); |
There was a problem hiding this comment.
Race condition: two clicks within 300ms → both timers fire, first tooltip flashes before being redrawn for the second.
Browser repro: appointments at 8:00 and 10:00, click A then B within 300ms — onAppointmentTooltipShowing fires twice.
Failing test for appointments_new.test.ts:
it('REPRO race: no tooltip for first appointment after clicking another within 300ms', async () => {
const { POM } = await createScheduler({
dataSource: [
{ text: 'A', startDate: new Date(2015, 1, 9, 8), endDate: new Date(2015, 1, 9, 9) },
{ text: 'B', startDate: new Date(2015, 1, 9, 10), endDate: new Date(2015, 1, 9, 11) },
],
currentView: 'day',
currentDate: new Date(2015, 1, 9, 8),
});
const a = POM.getAppointments().find((x) => x.getText() === 'A')!.element!;
const b = POM.getAppointments().find((x) => x.getText() === 'B')!.element!;
jest.useFakeTimers();
a.click();
jest.advanceTimersByTime(100);
b.click();
jest.advanceTimersByTime(200);
const after = POM.tooltip.isVisible() ? POM.tooltip.getAppointmentItem(0).textContent : null;
jest.advanceTimersByTime(200);
expect(POM.tooltip.getAppointmentItem(0).textContent).toContain('B');
expect(after).toBeNull(); // FAILS: received "A8:00 AM - 9:00 AM"
});Need to clear the previous timer before scheduling a new one.
There was a problem hiding this comment.
I noticed that the old impl has the same problem. But I have applied fix for the new impl and added a unit test
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| if ((e as any).cancel) { | ||
| return; | ||
| } |
There was a problem hiding this comment.
If preventSingleAppointmentClick was set by a prior dblclick and the next click is cancelled, this early return skips the setTimeout that resets the flag. The next normal click then hits preventSingleAppointmentClick === true and silently skips showAppointmentTooltip.
Can't repro in browser — race above masks it (a leftover timer eventually resets the flag). Reproduces in jest:
Expected: true (tooltip should be visible after the normal click)
Received: false
Once race is fixed, this surfaces. Can you double-check? Looks like cancel branch should clear pending timer + reset the flag.
There was a problem hiding this comment.
fixed and added a unit test
| private appointmentClickTimeout: number | null = null; | ||
|
|
||
| private preventSingleAppointmentClick = false; |
There was a problem hiding this comment.
Timer + flag on Appointments feel out of place. Debounce-before-tooltip is tooltip behavior, not collection state. Could discuss separately?
There was a problem hiding this comment.
I have removed the flag, now it's only a timer. I am not sure if we need to move this logic to the tooltip, cuz it's logic may become complicated.
Right now tooltip is "dumb": it doesn't know about appointments and clicks, which I think is good
| // @ts-expect-error | ||
| items: true, | ||
| // @ts-expect-error | ||
| targetedAppointmentData: true, |
There was a problem hiding this comment.
every @ts-expect-error — should have comments
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| if ((e as any).cancel) { |
There was a problem hiding this comment.
(e as any).cancel can be replaced with (e as Cancelable).cancel.
Cancelable is exported from @js/common/core/events:
import type { Cancelable } from '@js/common/core/events';
// ...
if ((e as Cancelable).cancel) {
return;
}| showAppointmentTooltipCore: ( | ||
| target: dxElementWrapper, | ||
| data: AppointmentTooltipItem[], | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| options?: any, | ||
| ) => void; |
There was a problem hiding this comment.
AppointmentTooltipExtraOptions is the right type here.
It's already defined in tooltip_strategies/tooltip_strategy_base.ts:70
| super._setOptionsByReference(); | ||
|
|
||
| // Note: items have appointmentData, which is used as a key in dataSource | ||
| this._optionsByReference = { |
There was a problem hiding this comment.
Why do we need the _setOptionsByReference() method?
Can you explain?
There was a problem hiding this comment.
Because to access appointment in data source (e.g. to delete or update it), the scheduler uses appointmentData object reference as a key in data source.
This PR adds functionality to show appointment popup, which gets appointmentData as its parameter. When appointment popup saves data it uses the same appointmentData object to update data in the data source.
OptionManager (aka .option()) deep-copies all the options passed to the component. So in order to not lose the object reference, we need to pass appointmentData as a reference for both appointmentView and collector.
There was a problem hiding this comment.
This method will be removed when usage of DOMComponent will be removed from AppointmentView and Collector
| 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); | ||
| } | ||
|
|
There was a problem hiding this comment.
onAppointmentClick fires here only when isButtonClick: true (i.e. tooltip opened via collector). When tooltip is opened via direct appointment click does clicking an item inside it also fire onAppointmentClick? If not, is that intentional?
There was a problem hiding this comment.
Legacy implementation doesn't fire onAppointmentClick when tooltip is opened on appointment click, so to avoid any breaking changes I have made the behavior similar in the refactoring.
But in general, I think we need to rethink how onAppointmentClick works due to the reasons I have described in this comment.
Thanks for noticing, I will take it into account after the appointments refactoring is done
No description provided.