Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -383,6 +383,40 @@ export const ResourcesColumnLayout: Story = {
},
};

export const CustomItemBeforeMainGroup: Story = {
args: {
...baseConfig,
resources,
},
argTypes: iconsShowModeArgType,
render: (args) => {
return (
<Scheduler
{...baseConfig}
resources={resources}
editing={{
form: {
items: [
{
name: "customNotice",
template: () => {
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,
Expand Down
10 changes: 10 additions & 0 deletions apps/react-storybook/stories/scheduler/form-customization.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import DOMComponent from '@ts/core/widget/dom_component';

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

type ClassRects = Record<string, Partial<DOMRect>>;

interface SetupSchedulerTestEnvironmentOptions {
width?: number;
height?: number;
classRects?: ClassRects;
}

export const DEFAULT_CELL_WIDTH = 250;
Expand All @@ -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);
Expand All @@ -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;
});
};
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It wasn't clear to me why animationTop should be 50px if there's no form customization, until I saw that getBoundingClientRect is mocked with some special values.

I suggest to write a functional testcafe test with form customization, so that we wouldn't need to mock getBoundingClient rect and the test would become more clear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer Jest here — CSS animation timing makes TestCafe tests flaky + any changed css or fonts break TestCafe test.
The test verifies the --dx-scheduler-animation-top value is calculated correctly, which is the actual behavior we care about.
The mock makes the expected value deterministic. If you feel the mock setup needs more clarity, happy to add a comment explaining the mocked rect values.

I suggest extending setupSchedulerTestEnvironment with classRects option so the rect values are explicit right in the test:

it('should set animation offset when custom item is placed before mainGroup', async () => {
  setupSchedulerTestEnvironment({
    height: 600,
    classRects: {
      'dx-scheduler-form-main-group': { top: 50, height: 50, bottom: 100 },
    },
  });

  const { scheduler, POM } = await createScheduler(getDefaultConfig());

  scheduler.showAppointmentPopup();
  POM.popup.selectRepeatValue('weekly');

  const animationTop = POM.popup.dxForm.element().style.getPropertyValue('--dx-scheduler-animation-top');
  expect(animationTop).toBe('50px'); // mainGroup starts 50px from form top
});

});
});

describe('firstDayOfWeek', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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 });
Expand Down
Loading