Skip to content

Commit 6e473f4

Browse files
Scheduler — Fix slide animation overlapping items outside mainGroup (#32955)
1 parent 522cf4e commit 6e473f4

6 files changed

Lines changed: 103 additions & 28 deletions

File tree

apps/react-storybook/stories/scheduler/SchedulerFormCustomization.stories.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,40 @@ export const ResourcesColumnLayout: Story = {
383383
},
384384
};
385385

386+
export const CustomItemBeforeMainGroup: Story = {
387+
args: {
388+
...baseConfig,
389+
resources,
390+
},
391+
argTypes: iconsShowModeArgType,
392+
render: (args) => {
393+
return (
394+
<Scheduler
395+
{...baseConfig}
396+
resources={resources}
397+
editing={{
398+
form: {
399+
items: [
400+
{
401+
name: "customNotice",
402+
template: () => {
403+
const element = document.createElement("div");
404+
element.className = "custom-form-notice";
405+
element.textContent = "This is a custom element placed before mainGroup. The slide animation should not overlap this area.";
406+
return element;
407+
},
408+
},
409+
"mainGroup",
410+
"recurrenceGroup",
411+
],
412+
iconsShowMode: args["editing.form.iconsShowMode"],
413+
},
414+
} as Properties["editing"]}
415+
/>
416+
);
417+
},
418+
};
419+
386420
export const RTL: Story = {
387421
args: {
388422
...baseConfig,

apps/react-storybook/stories/scheduler/form-customization.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@
99
.scheduler-form-custom-icon-button * {
1010
padding: 0 !important;
1111
}
12+
13+
.custom-form-notice {
14+
background: #fce4e4;
15+
border: 1px solid #e0a0a0;
16+
border-radius: 4px;
17+
padding: 8px 12px;
18+
color: #8b3a3a;
19+
font-size: 13px;
20+
line-height: 1.4;
21+
}

packages/devextreme-scss/scss/widgets/base/scheduler/_index.scss

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ $scheduler-appointment-form-label-padding: 20px;
484484
&.dx-scheduler-form-main-group-hidden {
485485
transform: translateX(-110%);
486486
position: absolute;
487-
top: 0;
487+
top: var(--dx-scheduler-animation-top, 0);
488488
}
489489
}
490490

@@ -497,7 +497,7 @@ $scheduler-appointment-form-label-padding: 20px;
497497
&.dx-scheduler-form-recurrence-group-hidden {
498498
transform: translateX(110%);
499499
position: absolute;
500-
top: 0;
500+
top: var(--dx-scheduler-animation-top, 0);
501501
}
502502
}
503503

packages/devextreme/js/__internal/scheduler/__tests__/__mock__/m_mock_scheduler.ts

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import DOMComponent from '@ts/core/widget/dom_component';
44

55
import SchedulerWorkSpace from '../../workspaces/m_work_space';
66

7+
type ClassRects = Record<string, Partial<DOMRect>>;
8+
79
interface SetupSchedulerTestEnvironmentOptions {
810
width?: number;
911
height?: number;
12+
classRects?: ClassRects;
1013
}
1114

1215
export const DEFAULT_CELL_WIDTH = 250;
@@ -16,6 +19,7 @@ export const DEFAULT_TIMELINE_CELL_HEIGHT = 450;
1619
export const setupSchedulerTestEnvironment = ({
1720
width = DEFAULT_CELL_WIDTH,
1821
height = DEFAULT_CELL_HEIGHT,
22+
classRects = {},
1923
}: SetupSchedulerTestEnvironmentOptions = {}): void => {
2024
jest.spyOn(logger, 'warn').mockImplementation(() => {});
2125
DOMComponent.prototype._isVisible = jest.fn((): boolean => true);
@@ -42,34 +46,28 @@ export const setupSchedulerTestEnvironment = ({
4246
return styles;
4347
});
4448

49+
const defaultRect: DOMRect = {
50+
width: 0, height: 0, top: 0, left: 0, bottom: 0, right: 0, x: 0, y: 0, toJSON: (): void => {},
51+
};
52+
53+
const cellRect = {
54+
width, height, bottom: height, right: width,
55+
};
56+
57+
const mergedRects: ClassRects = {
58+
'dx-scheduler-date-table-cell': cellRect,
59+
'dx-scheduler-all-day-table-cell': cellRect,
60+
...classRects,
61+
};
62+
4563
Element.prototype.getBoundingClientRect = jest.fn(function (): DOMRect {
4664
const classList: string[] = Array.from(this.classList);
47-
switch (true) {
48-
case classList.includes('dx-scheduler-date-table-cell')
49-
|| classList.includes('dx-scheduler-all-day-table-cell'):
50-
return {
51-
width,
52-
height,
53-
top: 0,
54-
left: 0,
55-
bottom: height,
56-
right: width,
57-
x: 0,
58-
y: 0,
59-
toJSON: (): void => {},
60-
};
61-
default:
62-
return {
63-
width: 0,
64-
height: 0,
65-
top: 0,
66-
left: 0,
67-
bottom: 0,
68-
right: 0,
69-
x: 0,
70-
y: 0,
71-
toJSON: (): void => {},
72-
};
65+
66+
const matchedClass = classList.find((className) => mergedRects[className]);
67+
if (matchedClass) {
68+
return { ...defaultRect, ...mergedRects[matchedClass] };
7369
}
70+
71+
return defaultRect;
7472
});
7573
};

packages/devextreme/js/__internal/scheduler/appointment_popup/appointment_popup.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1594,6 +1594,24 @@ describe('Appointment Form', () => {
15941594
expect(document.activeElement).toBe(frequencyEditorInputElement);
15951595
});
15961596
});
1597+
1598+
it('should set animation offset CSS variable when switching to recurrence form', async () => {
1599+
setupSchedulerTestEnvironment({
1600+
height: 600,
1601+
classRects: {
1602+
'dx-form': { top: 10 },
1603+
'dx-scheduler-form-main-group': { top: 60 },
1604+
},
1605+
});
1606+
1607+
const { scheduler, POM } = await createScheduler(getDefaultConfig());
1608+
1609+
scheduler.showAppointmentPopup();
1610+
POM.popup.selectRepeatValue('weekly');
1611+
1612+
const animationTop = POM.popup.dxForm.$element()[0].style.getPropertyValue('--dx-scheduler-animation-top');
1613+
expect(animationTop).toBe('50px');
1614+
});
15971615
});
15981616

15991617
describe('firstDayOfWeek', () => {

packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,8 @@ export class AppointmentForm {
926926
repeatEditor.close();
927927
}
928928

929+
this.updateAnimationOffset();
930+
929931
const currentHeight = this.dxPopup.option('height') as string | number | undefined;
930932

931933
if (currentHeight === 'auto' || currentHeight === undefined) {
@@ -1065,6 +1067,19 @@ export class AppointmentForm {
10651067
this.dxForm.endUpdate();
10661068
}
10671069

1070+
private updateAnimationOffset(): void {
1071+
if (!this._$mainGroup) {
1072+
return;
1073+
}
1074+
1075+
const formElement = this.dxForm.$element()[0];
1076+
const mainGroupElement = this._$mainGroup[0];
1077+
const formRect = formElement.getBoundingClientRect();
1078+
const groupRect = mainGroupElement.getBoundingClientRect();
1079+
const topOffset = groupRect.top - formRect.top;
1080+
formElement.style.setProperty('--dx-scheduler-animation-top', `${topOffset}px`);
1081+
}
1082+
10681083
private focusFirstFocusableInGroup($group: dxElementWrapper): void {
10691084
const focusTarget = $group.find(`.${CLASSES.fieldItemContent} [tabindex]`).first().get(0) as HTMLElement;
10701085
focusTarget?.focus({ preventScroll: true });

0 commit comments

Comments
 (0)