Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8e5d5b6
refactor(scheduler): add resize date math to new appointments collection
bit-byte0 Jun 30, 2026
bb4bf3f
feat(resizable): add onCancelByEsc to cancel resize via Escape
bit-byte0 Jun 30, 2026
7b834c8
refactor(scheduler): add resize computation helpers for new appointments
bit-byte0 Jul 1, 2026
ed74fac
feat(scheduler): render resizable handles on new grid appointments
bit-byte0 Jul 1, 2026
c710769
feat(scheduler): resize timed appointments in the new collection
bit-byte0 Jul 1, 2026
c9ffdc4
feat(scheduler): focus resized appointment on resize start
bit-byte0 Jul 1, 2026
46cacc5
refactor(scheduler): remove unused all-day resize helper
bit-byte0 Jul 1, 2026
b836220
fix(scheduler): address resize PR review comments
bit-byte0 Jul 1, 2026
21b20d3
fix(resizable): use internal events engine in test to satisfy check-t…
bit-byte0 Jul 1, 2026
531e642
test(scheduler): dispose resizable widgets in test and fix delta test…
bit-byte0 Jul 1, 2026
2e91222
fix(resizable): keep resize on dragend without dragstart (esc-cancel …
bit-byte0 Jul 1, 2026
ec46838
fix(scheduler): bound appointment resize to scrollable area and reset…
bit-byte0 Jul 1, 2026
0d1b676
feat(scheduler): support all-day appointment resize in the new collec…
bit-byte0 Jul 1, 2026
0046452
refactor(scheduler): type resizable test options and drop redundant r…
bit-byte0 Jul 1, 2026
525a711
fix(scheduler): constrain resize to group bounds and roll back on can…
bit-byte0 Jul 1, 2026
81375a4
fix(scheduler): guard appointment resize end against missing settings
bit-byte0 Jul 3, 2026
1ac432d
refactor(resizable): rework esc-cancel per review
bit-byte0 Jul 7, 2026
bdc0cb7
fix(scheduler): only enable appointment resize when handles exist
bit-byte0 Jul 7, 2026
8f7b2f2
fix(resizable): avoid duplicate keydown subscription on re-render
bit-byte0 Jul 7, 2026
604e50d
fix(resizable): handle cancelOnEscape option change
bit-byte0 Jul 7, 2026
ad8bc30
refactor(resizable): make test model DOM-based per review
bit-byte0 Jul 8, 2026
2e7a17e
refactor(resizable): unbind keydown in _clean and document esc-cancel…
bit-byte0 Jul 8, 2026
04c90ee
Merge branch '25_2' into refactor/scheduler-appointments-resizing-25_2
bit-byte0 Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import {
afterEach, beforeEach, describe, expect, it,
} from '@jest/globals';
import fx from '@js/common/core/animation/fx';
import $ from '@js/core/renderer';
import type { Properties } from '@js/ui/scheduler';
import eventsEngine from '@ts/events/core/m_events_engine';

import { createScheduler as baseCreateScheduler } from './__mock__/create_scheduler';
import { setupSchedulerTestEnvironment } from './__mock__/mock_scheduler';

const createScheduler = (config: Properties):
ReturnType<typeof baseCreateScheduler> => baseCreateScheduler({
...config,
// eslint-disable-next-line @typescript-eslint/naming-convention
_newAppointments: true,
});

const RESIZE_HANDLE_SELECTOR = '.dx-resizable-handle';

const baseConfig: Properties = {
views: ['week'],
currentView: 'week',
currentDate: new Date(2024, 0, 1),
height: 600,
};

const timedAppointment = {
text: 'Timed',
startDate: new Date(2024, 0, 1, 9, 0),
endDate: new Date(2024, 0, 1, 10, 0),
};

const countResizeHandles = (element: Element | null | undefined): number => (
element?.querySelectorAll(RESIZE_HANDLE_SELECTOR).length ?? 0
);

describe('Appointments Resizing', () => {
beforeEach(() => {
setupSchedulerTestEnvironment();
fx.off = true;
});

afterEach(() => {
const $scheduler = $('.dx-scheduler');
// @ts-expect-error
$scheduler.dxScheduler('dispose');
document.body.innerHTML = '';
fx.off = false;
});

it('should render resize handles on a grid appointment when resizing is allowed', async () => {
const { POM } = await createScheduler({
...baseConfig,
dataSource: [timedAppointment],
editing: { allowUpdating: true, allowResizing: true },
});

expect(countResizeHandles(POM.getAppointment('Timed').element)).toBeGreaterThan(0);
});

it('should not render resize handles when resizing is disabled', async () => {
const { POM } = await createScheduler({
...baseConfig,
dataSource: [timedAppointment],
editing: { allowUpdating: true, allowResizing: false },
});

expect(countResizeHandles(POM.getAppointment('Timed').element)).toBe(0);
});

it('should render horizontal resize handles on all-day appointments', async () => {
const { POM } = await createScheduler({
...baseConfig,
dataSource: [{ ...timedAppointment, text: 'AllDay', allDay: true }],
editing: { allowUpdating: true, allowResizing: true },
});

const { element } = POM.getAppointment('AllDay');

expect(countResizeHandles(element)).toBeGreaterThan(0);
expect(element?.querySelectorAll('.dx-resizable-handle-left, .dx-resizable-handle-right').length)
.toBeGreaterThan(0);
});

it('should not render resize handles on all-day appointments when resizing is disabled', async () => {
const { POM } = await createScheduler({
...baseConfig,
dataSource: [{ ...timedAppointment, text: 'AllDay', allDay: true }],
editing: { allowUpdating: true, allowResizing: false },
});

expect(countResizeHandles(POM.getAppointment('AllDay').element)).toBe(0);
});

it('should focus the appointment on resize start', async () => {
const { POM } = await createScheduler({
...baseConfig,
dataSource: [timedAppointment],
editing: { allowUpdating: true, allowResizing: true },
});

const appointment = POM.getAppointment('Timed').element as HTMLElement;
const handle = appointment.querySelector(RESIZE_HANDLE_SELECTOR) as HTMLElement;

eventsEngine.trigger(handle, { type: 'dxdragstart', target: handle });

expect(document.activeElement).toBe(appointment);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,28 @@ describe('GridAppointment', () => {
});
});

describe('Resize', () => {
it('should render resize handles when allowResize is true', async () => {
const instance = await createGridAppointment({
...getProperties(defaultAppointmentData),
allowResize: true,
resizableConfig: { handles: 'top bottom' },
});

expect(instance.$element().find('.dx-resizable-handle').length).toBeGreaterThan(0);
});

it('should not render resize handles when allowResize is false', async () => {
const instance = await createGridAppointment({
...getProperties(defaultAppointmentData),
allowResize: false,
resizableConfig: { handles: 'top bottom' },
});

expect(instance.$element().find('.dx-resizable-handle').length).toBe(0);
});
});

describe('Resources', () => {
it('should apply resource color', async () => {
const instance = await createGridAppointment({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import Resizable, { type ResizableProperties } from '@ts/ui/resizable/resizable';

import {
ALL_DAY_TEXT, APPOINTMENT_CLASSES, APPOINTMENT_TYPE_CLASSES, RECURRING_LABEL,
Expand All @@ -17,16 +18,28 @@ export interface GridAppointmentViewProperties extends BaseAppointmentViewProper
modifiers: {
empty: boolean;
};
allowResize?: boolean;
resizableConfig?: ResizableProperties;
}

export class GridAppointmentView extends BaseAppointmentView<GridAppointmentViewProperties> {
override _initMarkup(): void {
super._initMarkup();

this.renderResizable();

// eslint-disable-next-line no-void
void this.applyElementColor();
}

private renderResizable(): void {
if (!this.option().allowResize) {
return;
}

this._createComponent(this.$element(), Resizable, this.option().resizableConfig ?? {});
}

public override resize(
geometry?: { height: number; width: number; top: number; left: number },
): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ export class AppointmentsFocusController {
}
}

private focusViewItem(viewItem: ViewItem): void {
public focusViewItem(viewItem: ViewItem): void {
this.resetTabIndex(viewItem.option().sortedIndex);
focus.trigger(viewItem?.$element());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const getProperties = (options: {
allowDelete: false,
onDeleteKeyPress: (): void => {},
focusFallbackAfterDelete: (): void => {},
getResizableConfig: () => undefined,
});

const createAppointments = (
Expand Down Expand Up @@ -409,6 +410,27 @@ describe('Appointments', () => {
});
});

describe('Resize', () => {
it('should restore view model geometry on resetAppointmentResize', () => {
const instance = createAppointments(getProperties());
instance.option('viewModel', [
mockGridViewModel(defaultAppointmentData, {
sortedIndex: 0, top: 10, left: 20, height: 50, width: 100,
}),
]);

const $element = instance.getViewItemBySortedIndex(0)?.$element() as ReturnType<typeof $>;
$element.css({ height: '999px', width: '888px', top: '1px' });

instance.resetAppointmentResize($element);

expect($element.css('height')).toBe('50px');
expect($element.css('width')).toBe('100px');
expect($element.css('top')).toBe('10px');
expect($element.css('left')).toBe('20px');
});
});

describe('Resources', () => {
it('should apply resource color', async () => {
const instance = createAppointments({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { isElementInDom } from '@ts/core/utils/m_dom';
import type { DOMComponentProperties } from '@ts/core/widget/dom_component';
import DOMComponent from '@ts/core/widget/dom_component';
import type { OptionChanged } from '@ts/core/widget/types';
import type { ResizableProperties } from '@ts/ui/resizable/resizable';

import type { AppointmentTooltipExtraOptions } from '../tooltip_strategies/tooltip_strategy_base';
import type {
Expand Down Expand Up @@ -86,6 +87,10 @@ export interface AppointmentsProperties extends DOMComponentProperties<Appointme
targetedAppointmentData: TargetedAppointment;
}) => void;
focusFallbackAfterDelete: () => void;

getResizableConfig: (
viewModel: AppointmentItemViewModel,
) => ResizableProperties | undefined;
}

export class Appointments extends DOMComponent<Appointments, AppointmentsProperties> {
Expand Down Expand Up @@ -123,6 +128,18 @@ export class Appointments extends DOMComponent<Appointments, AppointmentsPropert
return this.getViewModelBySortedIndex(viewItem.option().sortedIndex);
}

public focusResizingAppointment($element: dxElementWrapper): void {
const viewItem = this.findViewItemByElement($element);

if (viewItem) {
this.focusController.focusViewItem(viewItem);
}
}

public resetAppointmentResize($element: dxElementWrapper): void {
this.findViewItemByElement($element)?.resize();
}

public getAppointmentData($element: dxElementWrapper): {
appointmentData: SafeAppointment,
targetedAppointmentData: TargetedAppointment,
Expand Down Expand Up @@ -194,6 +211,7 @@ export class Appointments extends DOMComponent<Appointments, AppointmentsPropert
allowDelete: false,
onDeleteKeyPress: noop,
focusFallbackAfterDelete: noop,
getResizableConfig: () => undefined,
};
}

Expand Down Expand Up @@ -393,6 +411,9 @@ export class Appointments extends DOMComponent<Appointments, AppointmentsPropert
};

if (isGridAppointmentViewModel(appointmentViewModel)) {
const resizableConfig = this.option().getResizableConfig(appointmentViewModel);
const allowResize = Boolean(resizableConfig?.handles) && resizableConfig?.handles !== 'none';

return this._createComponent(
$element,
GridAppointmentView,
Expand All @@ -407,6 +428,8 @@ export class Appointments extends DOMComponent<Appointments, AppointmentsPropert
modifiers: {
empty: appointmentViewModel.empty,
},
allowResize,
resizableConfig,
},
);
}
Expand Down
Loading
Loading