From 7b1425c712b0a822d453b11e3687894cb868e21e Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Wed, 18 Mar 2026 11:30:03 -0300 Subject: [PATCH 1/5] =?UTF-8?q?Scheduler=20New=20Form=20=E2=80=94=20Fix=20?= =?UTF-8?q?slide=20animation=20overlapping=20items=20outside=20mainGroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SchedulerFormCustomization.stories.tsx | 34 +++++++++++++++++++ .../stories/scheduler/form-customization.css | 10 ++++++ .../scss/widgets/base/scheduler/_index.scss | 4 +-- .../__tests__/__mock__/m_mock_scheduler.ts | 12 +++++++ .../appointment_popup.test.ts | 12 +++++++ .../scheduler/appointment_popup/m_form.ts | 19 +++++++++++ 6 files changed, 89 insertions(+), 2 deletions(-) diff --git a/apps/react-storybook/stories/scheduler/SchedulerFormCustomization.stories.tsx b/apps/react-storybook/stories/scheduler/SchedulerFormCustomization.stories.tsx index af535c78593f..7ab3463fa9ce 100644 --- a/apps/react-storybook/stories/scheduler/SchedulerFormCustomization.stories.tsx +++ b/apps/react-storybook/stories/scheduler/SchedulerFormCustomization.stories.tsx @@ -383,6 +383,40 @@ export const ResourcesColumnLayout: Story = { }, }; +export const CustomItemBeforeMainGroup: Story = { + args: { + ...baseConfig, + resources, + }, + argTypes: iconsShowModeArgType, + render: (args) => { + return ( + { + const element = document.createElement("div"); + element.className = "custom-form-notice"; + element.textContent = "This is a custom element placed before mainGroup. The slide animation should not overlap this area."; + return element; + }, + }, + "mainGroup", + "recurrenceGroup", + ], + iconsShowMode: args["editing.form.iconsShowMode"], + }, + } as Properties["editing"]} + /> + ); + }, +}; + export const RTL: Story = { args: { ...baseConfig, diff --git a/apps/react-storybook/stories/scheduler/form-customization.css b/apps/react-storybook/stories/scheduler/form-customization.css index 18bed5cdf7cd..c7bef1f82e21 100644 --- a/apps/react-storybook/stories/scheduler/form-customization.css +++ b/apps/react-storybook/stories/scheduler/form-customization.css @@ -9,3 +9,13 @@ .scheduler-form-custom-icon-button * { padding: 0 !important; } + +.custom-form-notice { + background: #fce4e4; + border: 1px solid #e0a0a0; + border-radius: 4px; + padding: 8px 12px; + color: #8b3a3a; + font-size: 13px; + line-height: 1.4; +} diff --git a/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss index de7a07be1ec4..b39a619d4d19 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss @@ -484,7 +484,7 @@ $scheduler-appointment-form-label-padding: 20px; &.dx-scheduler-form-main-group-hidden { transform: translateX(-110%); position: absolute; - top: 0; + top: var(--dx-scheduler-animation-top, 0); } } @@ -497,7 +497,7 @@ $scheduler-appointment-form-label-padding: 20px; &.dx-scheduler-form-recurrence-group-hidden { transform: translateX(110%); position: absolute; - top: 0; + top: var(--dx-scheduler-animation-top, 0); } } diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts index 7fb76ab65d0b..496444bf0349 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts @@ -58,6 +58,18 @@ export const setupSchedulerTestEnvironment = ({ y: 0, toJSON: (): void => {}, }; + case classList.includes('dx-scheduler-form-main-group'): + return { + width: 0, + height: 0, + top: 50, + left: 0, + bottom: 50, + right: 0, + x: 0, + y: 50, + toJSON: (): void => {}, + }; default: return { width: 0, diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts index 524423b082d1..a29a0b41f9e5 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts @@ -1594,6 +1594,18 @@ describe('Appointment Form', () => { expect(document.activeElement).toBe(frequencyEditorInputElement); }); }); + + it('should set animation offset CSS variable when switching to recurrence form', async () => { + const { scheduler, POM } = await createScheduler(getDefaultConfig()); + + scheduler.showAppointmentPopup(); + + POM.popup.selectRepeatValue('weekly'); + + const formElement = POM.popup.dxForm.$element()[0]; + const animationTop = formElement.style.getPropertyValue('--dx-scheduler-animation-top'); + expect(animationTop).toBe('50px'); + }); }); describe('firstDayOfWeek', () => { diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts index f525182dedaa..56ff1e0493ff 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -926,6 +926,9 @@ export class AppointmentForm { repeatEditor.close(); } + this._popup.updateToolbarForRecurrenceGroup(); + this.updateAnimationOffset(); + const currentHeight = this.dxPopup.option('height') as string | number | undefined; if (currentHeight === 'auto' || currentHeight === undefined) { @@ -1065,6 +1068,22 @@ export class AppointmentForm { this.dxForm.endUpdate(); } + private updateAnimationOffset(): void { + if (!this._$mainGroup) { + return; + } + + const formElement = this.dxForm.$element()[0]; + const mainGroupElement = this._$mainGroup[0]; + + if (formElement && mainGroupElement) { + const formRect = formElement.getBoundingClientRect(); + const groupRect = mainGroupElement.getBoundingClientRect(); + const topOffset = groupRect.top - formRect.top; + formElement.style.setProperty('--dx-scheduler-animation-top', `${topOffset}px`); + } + } + private focusFirstFocusableInGroup($group: dxElementWrapper): void { const focusTarget = $group.find(`.${CLASSES.fieldItemContent} [tabindex]`).first().get(0) as HTMLElement; focusTarget?.focus({ preventScroll: true }); From 5d1afed130906b30f70e37811186c66b2fb1dc8b Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 19 Mar 2026 05:07:59 -0300 Subject: [PATCH 2/5] =?UTF-8?q?Scheduler=20New=20Form=20=E2=80=94=20Addres?= =?UTF-8?q?s=20review:=20use=20element(),=20simplify=20updateAnimationOffs?= =?UTF-8?q?et?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../appointment_popup/appointment_popup.test.ts | 2 +- .../scheduler/appointment_popup/m_form.ts | 13 +++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts index a29a0b41f9e5..edb132f9899f 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts @@ -1602,7 +1602,7 @@ describe('Appointment Form', () => { POM.popup.selectRepeatValue('weekly'); - const formElement = POM.popup.dxForm.$element()[0]; + const formElement = POM.popup.dxForm.element(); const animationTop = formElement.style.getPropertyValue('--dx-scheduler-animation-top'); expect(animationTop).toBe('50px'); }); diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts index 56ff1e0493ff..4f57f310996b 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -1073,15 +1073,12 @@ export class AppointmentForm { return; } - const formElement = this.dxForm.$element()[0]; + const formElement = this.dxForm.element(); const mainGroupElement = this._$mainGroup[0]; - - if (formElement && mainGroupElement) { - const formRect = formElement.getBoundingClientRect(); - const groupRect = mainGroupElement.getBoundingClientRect(); - const topOffset = groupRect.top - formRect.top; - formElement.style.setProperty('--dx-scheduler-animation-top', `${topOffset}px`); - } + const formRect = formElement.getBoundingClientRect(); + const groupRect = mainGroupElement.getBoundingClientRect(); + const topOffset = groupRect.top - formRect.top; + formElement.style.setProperty('--dx-scheduler-animation-top', `${topOffset}px`); } private focusFirstFocusableInGroup($group: dxElementWrapper): void { From 1efe9f61fb4a4dffd41b11ad696b61c6c77f2a50 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 19 Mar 2026 09:33:46 -0300 Subject: [PATCH 3/5] =?UTF-8?q?Scheduler=20New=20Form=20=E2=80=94=20Improv?= =?UTF-8?q?e=20animation=20offset=20test:=20use=20classRects=20in=20setupS?= =?UTF-8?q?chedulerTestEnvironment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/__mock__/m_mock_scheduler.ts | 38 ++++++++----------- .../appointment_popup.test.ts | 12 ++++-- 2 files changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts index 496444bf0349..b4aa2343a945 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts @@ -4,9 +4,12 @@ import DOMComponent from '@ts/core/widget/dom_component'; import SchedulerWorkSpace from '../../workspaces/m_work_space'; +type ClassRects = Record>; + interface SetupSchedulerTestEnvironmentOptions { width?: number; height?: number; + classRects?: ClassRects; } export const DEFAULT_CELL_WIDTH = 250; @@ -16,6 +19,7 @@ export const DEFAULT_TIMELINE_CELL_HEIGHT = 450; export const setupSchedulerTestEnvironment = ({ width = DEFAULT_CELL_WIDTH, height = DEFAULT_CELL_HEIGHT, + classRects = {}, }: SetupSchedulerTestEnvironmentOptions = {}): void => { jest.spyOn(logger, 'warn').mockImplementation(() => {}); DOMComponent.prototype._isVisible = jest.fn((): boolean => true); @@ -42,8 +46,18 @@ export const setupSchedulerTestEnvironment = ({ return styles; }); + const defaultRect: DOMRect = { + width: 0, height: 0, top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0, toJSON: (): void => {}, + }; + Element.prototype.getBoundingClientRect = jest.fn(function (): DOMRect { const classList: string[] = Array.from(this.classList); + + const matchedClass = classList.find((className) => classRects[className]); + if (matchedClass) { + return { ...defaultRect, ...classRects[matchedClass] }; + } + switch (true) { case classList.includes('dx-scheduler-date-table-cell') || classList.includes('dx-scheduler-all-day-table-cell'): @@ -58,30 +72,8 @@ export const setupSchedulerTestEnvironment = ({ y: 0, toJSON: (): void => {}, }; - case classList.includes('dx-scheduler-form-main-group'): - return { - width: 0, - height: 0, - top: 50, - left: 0, - bottom: 50, - right: 0, - x: 0, - y: 50, - toJSON: (): void => {}, - }; default: - return { - width: 0, - height: 0, - top: 0, - left: 0, - bottom: 0, - right: 0, - x: 0, - y: 0, - toJSON: (): void => {}, - }; + return defaultRect; } }); }; diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts index edb132f9899f..85a2b5177f1e 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts @@ -1596,14 +1596,20 @@ describe('Appointment Form', () => { }); it('should set animation offset CSS variable when switching to recurrence form', async () => { + setupSchedulerTestEnvironment({ + height: 600, + classRects: { + 'dx-form': { top: 10 }, + 'dx-scheduler-form-main-group': { top: 60 }, + }, + }); + const { scheduler, POM } = await createScheduler(getDefaultConfig()); scheduler.showAppointmentPopup(); - POM.popup.selectRepeatValue('weekly'); - const formElement = POM.popup.dxForm.element(); - const animationTop = formElement.style.getPropertyValue('--dx-scheduler-animation-top'); + const animationTop = POM.popup.dxForm.element().style.getPropertyValue('--dx-scheduler-animation-top'); expect(animationTop).toBe('50px'); }); }); From 40643b038dbeb01d99d274c97c2b11321c56aa62 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 19 Mar 2026 10:06:49 -0300 Subject: [PATCH 4/5] =?UTF-8?q?Scheduler=20New=20Form=20=E2=80=94=20Refact?= =?UTF-8?q?or=20mock:=20merge=20width/height=20into=20classRects=20for=20c?= =?UTF-8?q?ell=20elements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/__mock__/m_mock_scheduler.ts | 32 ++++++++----------- .../scheduler/appointment_popup/m_form.ts | 1 - 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts index b4aa2343a945..7a6fd5c14896 100644 --- a/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts @@ -50,30 +50,24 @@ export const setupSchedulerTestEnvironment = ({ width: 0, height: 0, top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0, toJSON: (): void => {}, }; + const cellRect = { + width, height, bottom: height, right: width, + }; + + const mergedRects: ClassRects = { + 'dx-scheduler-date-table-cell': cellRect, + 'dx-scheduler-all-day-table-cell': cellRect, + ...classRects, + }; + Element.prototype.getBoundingClientRect = jest.fn(function (): DOMRect { const classList: string[] = Array.from(this.classList); - const matchedClass = classList.find((className) => classRects[className]); + const matchedClass = classList.find((className) => mergedRects[className]); if (matchedClass) { - return { ...defaultRect, ...classRects[matchedClass] }; + return { ...defaultRect, ...mergedRects[matchedClass] }; } - switch (true) { - case classList.includes('dx-scheduler-date-table-cell') - || classList.includes('dx-scheduler-all-day-table-cell'): - return { - width, - height, - top: 0, - left: 0, - bottom: height, - right: width, - x: 0, - y: 0, - toJSON: (): void => {}, - }; - default: - return defaultRect; - } + return defaultRect; }); }; diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts index 4f57f310996b..de64a16e5eec 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -926,7 +926,6 @@ export class AppointmentForm { repeatEditor.close(); } - this._popup.updateToolbarForRecurrenceGroup(); this.updateAnimationOffset(); const currentHeight = this.dxPopup.option('height') as string | number | undefined; From 164a1dcd6e01228ab36a5b2e242da480d82815f7 Mon Sep 17 00:00:00 2001 From: Aleksey Semikozov Date: Thu, 19 Mar 2026 12:10:26 -0300 Subject: [PATCH 5/5] =?UTF-8?q?Scheduler=20New=20Form=20=E2=80=94=20Fix=20?= =?UTF-8?q?slide=20animation=20overlapping=20items=20outside=20mainGroup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scheduler/appointment_popup/appointment_popup.test.ts | 2 +- .../js/__internal/scheduler/appointment_popup/m_form.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts index 85a2b5177f1e..dd4fd1eb565b 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts @@ -1609,7 +1609,7 @@ describe('Appointment Form', () => { scheduler.showAppointmentPopup(); POM.popup.selectRepeatValue('weekly'); - const animationTop = POM.popup.dxForm.element().style.getPropertyValue('--dx-scheduler-animation-top'); + const animationTop = POM.popup.dxForm.$element()[0].style.getPropertyValue('--dx-scheduler-animation-top'); expect(animationTop).toBe('50px'); }); }); diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts index de64a16e5eec..92cb97989d78 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -1072,7 +1072,7 @@ export class AppointmentForm { return; } - const formElement = this.dxForm.element(); + const formElement = this.dxForm.$element()[0]; const mainGroupElement = this._$mainGroup[0]; const formRect = formElement.getBoundingClientRect(); const groupRect = mainGroupElement.getBoundingClientRect();