Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5ed012f
refactor(scheduler): add resize date math to new appointments collection
bit-byte0 Jun 30, 2026
4bdf911
feat(resizable): add onCancelByEsc to cancel resize via Escape
bit-byte0 Jun 30, 2026
318fe99
refactor(scheduler): add resize computation helpers for new appointments
bit-byte0 Jul 1, 2026
a1c5a89
feat(scheduler): render resizable handles on new grid appointments
bit-byte0 Jul 1, 2026
cfb12c2
feat(scheduler): resize timed appointments in the new collection
bit-byte0 Jul 1, 2026
800ac47
feat(scheduler): focus resized appointment on resize start
bit-byte0 Jul 1, 2026
915c942
refactor(scheduler): remove unused all-day resize helper
bit-byte0 Jul 1, 2026
94f52e7
fix(scheduler): address resize PR review comments
bit-byte0 Jul 1, 2026
e8baf97
fix(resizable): use internal events engine in test to satisfy check-t…
bit-byte0 Jul 1, 2026
33c5a31
test(scheduler): dispose resizable widgets in test and fix delta test…
bit-byte0 Jul 1, 2026
56bb3a7
fix(resizable): keep resize on dragend without dragstart (esc-cancel …
bit-byte0 Jul 1, 2026
233c4c1
fix(scheduler): bound appointment resize to scrollable area and reset…
bit-byte0 Jul 1, 2026
3a332ed
feat(scheduler): support all-day appointment resize in the new collec…
bit-byte0 Jul 1, 2026
746dd86
refactor(scheduler): type resizable test options and drop redundant r…
bit-byte0 Jul 1, 2026
eca68a4
fix(scheduler): constrain resize to group bounds and roll back on can…
bit-byte0 Jul 1, 2026
b86b556
fix(scheduler): guard appointment resize end against missing settings
bit-byte0 Jul 3, 2026
c972eeb
refactor(resizable): rework esc-cancel per review
bit-byte0 Jul 7, 2026
19c5d92
fix(scheduler): only enable appointment resize when handles exist
bit-byte0 Jul 7, 2026
b097378
fix(resizable): avoid duplicate keydown subscription on re-render
bit-byte0 Jul 7, 2026
cb4b5df
fix(resizable): handle cancelOnEscape option change
bit-byte0 Jul 7, 2026
02e8d2d
refactor(resizable): make test model DOM-based per review
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