Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
bf2f87d
refactor(scheduler): add resize date math to new appointments collection
bit-byte0 Jun 30, 2026
4044279
feat(resizable): add onCancelByEsc to cancel resize via Escape
bit-byte0 Jun 30, 2026
5c9677e
refactor(scheduler): add resize computation helpers for new appointments
bit-byte0 Jul 1, 2026
901090b
feat(scheduler): render resizable handles on new grid appointments
bit-byte0 Jul 1, 2026
87ecab0
feat(scheduler): resize timed appointments in the new collection
bit-byte0 Jul 1, 2026
11b6179
feat(scheduler): focus resized appointment on resize start
bit-byte0 Jul 1, 2026
b2aab3d
refactor(scheduler): remove unused all-day resize helper
bit-byte0 Jul 1, 2026
8fc3a6a
fix(scheduler): address resize PR review comments
bit-byte0 Jul 1, 2026
b53bf97
fix(resizable): use internal events engine in test to satisfy check-t…
bit-byte0 Jul 1, 2026
9a97fe9
test(scheduler): dispose resizable widgets in test and fix delta test…
bit-byte0 Jul 1, 2026
8c97a40
fix(resizable): keep resize on dragend without dragstart (esc-cancel …
bit-byte0 Jul 1, 2026
f247dbb
fix(scheduler): bound appointment resize to scrollable area and reset…
bit-byte0 Jul 1, 2026
e3dcc0b
feat(scheduler): support all-day appointment resize in the new collec…
bit-byte0 Jul 1, 2026
0a4957a
refactor(scheduler): type resizable test options and drop redundant r…
bit-byte0 Jul 1, 2026
4457b01
fix(scheduler): constrain resize to group bounds and roll back on can…
bit-byte0 Jul 1, 2026
838d4ae
fix(scheduler): guard appointment resize end against missing settings
bit-byte0 Jul 3, 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 @@ -176,7 +176,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 @@ -56,6 +56,7 @@ const getProperties = (options: {
showEditAppointmentPopup: (): void => {},
allowDelete: false,
onDeleteKeyPress: (): void => {},
getResizableConfig: () => undefined,
});

const createAppointments = (
Expand Down Expand Up @@ -408,6 +409,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 @@ -85,6 +86,10 @@ export interface AppointmentsProperties extends DOMComponentProperties<Appointme
appointmentData: SafeAppointment;
targetedAppointmentData: TargetedAppointment;
}) => void;

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

export class Appointments extends DOMComponent<Appointments, AppointmentsProperties> {
Expand Down Expand Up @@ -122,6 +127,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 @@ -192,6 +209,7 @@ export class Appointments extends DOMComponent<Appointments, AppointmentsPropert
onAppointmentContextMenu: noop,
allowDelete: false,
onDeleteKeyPress: noop,
getResizableConfig: () => undefined,
};
}

Expand Down Expand Up @@ -391,6 +409,8 @@ export class Appointments extends DOMComponent<Appointments, AppointmentsPropert
};

if (isGridAppointmentViewModel(appointmentViewModel)) {
const resizableConfig = this.option().getResizableConfig(appointmentViewModel);

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