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 309239cf053d..bfbfa8b4b702 100644 --- a/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss @@ -486,7 +486,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); } } @@ -499,7 +499,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..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 @@ -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,34 +46,28 @@ 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 => {}, + }; + + 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); - 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 { - width: 0, - height: 0, - top: 0, - left: 0, - bottom: 0, - right: 0, - x: 0, - y: 0, - toJSON: (): void => {}, - }; + + const matchedClass = classList.find((className) => mergedRects[className]); + if (matchedClass) { + return { ...defaultRect, ...mergedRects[matchedClass] }; } + + 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 36fb06dfb3d4..b9b38df642b9 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,24 @@ describe('Appointment Form', () => { expect(document.activeElement).toBe(frequencyEditorInputElement); }); }); + + 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 animationTop = POM.popup.dxForm.$element()[0].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..92cb97989d78 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,8 @@ export class AppointmentForm { repeatEditor.close(); } + this.updateAnimationOffset(); + const currentHeight = this.dxPopup.option('height') as string | number | undefined; if (currentHeight === 'auto' || currentHeight === undefined) { @@ -1065,6 +1067,19 @@ export class AppointmentForm { this.dxForm.endUpdate(); } + private updateAnimationOffset(): void { + if (!this._$mainGroup) { + return; + } + + const formElement = this.dxForm.$element()[0]; + const mainGroupElement = this._$mainGroup[0]; + 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 });