Skip to content

Scheduler - Appointments Refactoring - Support click & double click#33633

Open
Tucchhaa wants to merge 3 commits into
DevExpress:26_1from
Tucchhaa:appt_click_26_1
Open

Scheduler - Appointments Refactoring - Support click & double click#33633
Tucchhaa wants to merge 3 commits into
DevExpress:26_1from
Tucchhaa:appt_click_26_1

Conversation

@Tucchhaa
Copy link
Copy Markdown
Contributor

No description provided.

@Tucchhaa Tucchhaa self-assigned this May 19, 2026
@Tucchhaa Tucchhaa requested a review from a team as a code owner May 19, 2026 12:17
Copilot AI review requested due to automatic review settings May 19, 2026 12:17
@Tucchhaa Tucchhaa added the 26_1 label May 19, 2026
super._dispose();

click.off(this.$element(), EVENTS_NAMESPACE);
dxClick.off(this.$element(), EVENTS_NAMESPACE);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old impl also uses dxClick event

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/onAppointmentRendered plumbing through Appointments, BaseAppointmentView, AppointmentCollector, and TooltipStrategyBase, including dxclick/dxdblclick wiring and tooltip-suppression timeout logic.
  • Refactor scheduler option handling: actions stored in this.actions, removing appointmentRenderedAction/createAppointmentRenderedAction; collector now consumes view models (items) instead of raw appointmentsData.
  • 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.

Comment on lines +508 to +512
{
dragBehavior: undefined, // TODO
isButtonClick: true,
tabFocusLoopEnabled: true,
},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

@Tucchhaa Tucchhaa May 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment on lines +451 to +462
// 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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +446 to +449
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((e as any).cancel) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +90 to +92
private appointmentClickTimeout: number | null = null;

private preventSingleAppointmentClick = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Timer + flag on Appointments feel out of place. Debounce-before-tooltip is tooltip behavior, not collection state. Could discuss separately?

Comment on lines +52 to +55
// @ts-expect-error
items: true,
// @ts-expect-error
targetedAppointmentData: true,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

every @ts-expect-error — should have comments

Comment on lines +446 to +447
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((e as any).cancel) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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;
}

Comment on lines +75 to +80
showAppointmentTooltipCore: (
target: dxElementWrapper,
data: AppointmentTooltipItem[],
// eslint-disable-next-line @typescript-eslint/no-explicit-any
options?: any,
) => void;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the _setOptionsByReference() method?
Can you explain?

Comment on lines +330 to +343
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);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants