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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import Scheduler from 'devextreme-testcafe-models/scheduler';
import { createWidget } from '../../../../helpers/createWidget';
import url from '../../../../helpers/getPageUrl';

fixture.disablePageReloads`Appointment Form: Functional`
.page(url(__dirname, '../../../container.html'));

const SCHEDULER_SELECTOR = '#container';

test('Subject text editor should have focus after returning from recurrence form', async (t) => {
const appointment = {
text: 'Appointment',
startDate: new Date('2021-04-26T16:30:00.000Z'),
endDate: new Date('2021-04-26T18:30:00.000Z'),
allDay: false,
recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10',
};

const scheduler = new Scheduler(SCHEDULER_SELECTOR);
const appointmentPopup = await scheduler.openAppointmentPopup(t, appointment, true);

await appointmentPopup.openRecurrenceSettings(t);

await t.click(appointmentPopup.recurrence.backButton);

await t
.expect(appointmentPopup.textEditor.getInput().focused)
.ok();
}).before(async () => {
await createWidget('dxScheduler', {
dataSource: [],
views: ['week'],
currentView: 'week',
currentDate: new Date(2021, 2, 25),
});
});

test('Recurrence start date editor should have focus after opening recurrence settings', async (t) => {
const appointment = {
text: 'Appointment',
startDate: new Date('2021-04-26T16:30:00.000Z'),
endDate: new Date('2021-04-26T18:30:00.000Z'),
allDay: false,
recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10',
};

const scheduler = new Scheduler(SCHEDULER_SELECTOR);
const appointmentPopup = await scheduler.openAppointmentPopup(t, appointment, true);

await appointmentPopup.openRecurrenceSettings(t);

await t
.expect(appointmentPopup.recurrence.startDateInput.focused)
.ok();
}).before(async () => {
await createWidget('dxScheduler', {
dataSource: [],
views: ['week'],
currentView: 'week',
currentDate: new Date(2021, 2, 25),
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -300,3 +300,40 @@ test.meta({ browserSize: [1500, 1500] })('appointment main form with opened star
currentDate: new Date(2021, 2, 25),
});
});

test.meta({ browserSize: [1500, 1500] })('Recurrence settings button should have correct focus state', async (t) => {
const { takeScreenshot, compareResults } = createScreenshotsComparer(t);

const appointment = {
text: 'Appointment',
startDate: new Date('2021-04-26T16:30:00.000Z'),
endDate: new Date('2021-04-26T18:30:00.000Z'),
allDay: false,
recurrenceRule: 'FREQ=WEEKLY;BYDAY=MO,TH;COUNT=10',
};

const scheduler = new Scheduler(SCHEDULER_SELECTOR);
const appointmentPopup = await scheduler.openAppointmentPopup(t, appointment, true);

await t
.click(appointmentPopup.repeatEditor.element)
.pressKey('tab');

await testScreenshot(
t,
takeScreenshot,
'scheduler__appointment__recurrence-settings-button__focus-state.png',
{ element: appointmentPopup.contentElement },
);

await t
.expect(compareResults.isValid())
.ok(compareResults.errorMessages());
}).before(async () => {
await createWidget('dxScheduler', {
dataSource: [],
views: ['week'],
currentView: 'week',
currentDate: new Date(2021, 2, 25),
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,11 @@ $fluent-scheduler-agenda-time-panel-cell-padding: 8px;

.dx-scheduler-form-repeat-editor .dx-scheduler-form-recurrence-settings-button {
height: auto;

&.dx-button-mode-text.dx-state-focused {
outline: auto;
outline-offset: -2px;
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1337,9 +1337,9 @@ describe('Appointment Form', () => {
scheduler.showAppointmentPopup();

expect(POM.popup.isMainGroupVisible()).toBe(true);
expect(POM.popup.mainGroup?.getAttribute('tabindex')).toBeNull();
expect(POM.popup.mainGroup?.getAttribute('inert')).toBeNull();
expect(POM.popup.isRecurrenceGroupVisible()).toBe(false);
expect(POM.popup.recurrenceGroup?.getAttribute('tabindex')).toBe('-1');
expect(POM.popup.recurrenceGroup?.getAttribute('inert')).toBe('true');

POM.popup.selectRepeatValue('weekly');
await new Promise(process.nextTick);
Expand All @@ -1349,17 +1349,17 @@ describe('Appointment Form', () => {
expect(typeof popupHeight).toBe('number');

expect(POM.popup.isMainGroupVisible()).toBe(false);
expect(POM.popup.mainGroup?.getAttribute('tabindex')).toBe('-1');
expect(POM.popup.mainGroup?.getAttribute('inert')).toBe('true');
expect(POM.popup.isRecurrenceGroupVisible()).toBe(true);
expect(POM.popup.recurrenceGroup?.getAttribute('tabindex')).toBeNull();
expect(POM.popup.recurrenceGroup?.getAttribute('inert')).toBeNull();

POM.popup.getBackButton().click();

expect(POM.popup.component.option('height')).toBe('auto');
expect(POM.popup.isMainGroupVisible()).toBe(true);
expect(POM.popup.mainGroup?.getAttribute('tabindex')).toBeNull();
expect(POM.popup.mainGroup?.getAttribute('inert')).toBeNull();
expect(POM.popup.isRecurrenceGroupVisible()).toBe(false);
expect(POM.popup.recurrenceGroup?.getAttribute('tabindex')).toBe('-1');
expect(POM.popup.recurrenceGroup?.getAttribute('inert')).toBe('true');
});

it('should open main form when opening recurring appointment', async () => {
Expand Down Expand Up @@ -1676,6 +1676,52 @@ describe('Appointment Form', () => {
});
});
});

describe('FrequencyEditor focus', () => {
it('should not be focused when value is changed via API', async () => {
const { POM, scheduler } = await createScheduler({
...getDefaultConfig(),
dataSource: [],
views: ['week'],
currentView: 'week',
currentDate: new Date(2021, 2, 25),
});

scheduler.showAppointmentPopup(recurringAppointment);
POM.popup.getEditSeriesButton().click();
POM.popup.openRecurrenceSettings();

const frequencyEditor = POM.popup.form.getEditor('recurrencePeriodEditor');
const frequencyEditorInputElement = POM.popup.getInput('recurrencePeriodEditor').get(0) as HTMLElement;

frequencyEditor?.option('value', 'yearly');

expect(document.activeElement).not.toBe(frequencyEditorInputElement);
});

it('should be focused when value is changed via keyboard', async () => {
const { POM, scheduler, keydown } = await createScheduler({
...getDefaultConfig(),
dataSource: [],
views: ['week'],
currentView: 'week',
currentDate: new Date(2021, 2, 25),
});

scheduler.showAppointmentPopup(recurringAppointment);
POM.popup.getEditSeriesButton().click();
POM.popup.openRecurrenceSettings();

const frequencyEditorInputElement = POM.popup.getInput('recurrencePeriodEditor').get(0) as HTMLElement;

frequencyEditorInputElement.click();
jest.useFakeTimers();
keydown(frequencyEditorInputElement, 'ArrowDown');
jest.runAllTimers();
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

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

The test uses jest.useFakeTimers() but doesn't restore real timers before completing. While the afterEach hook does restore real timers globally, it's better practice to restore timers explicitly at the end of the test to avoid potential test isolation issues. Consider adding jest.useRealTimers() after line 1720 or wrapping the fake timer usage in a try-finally block.

Suggested change
jest.runAllTimers();
jest.runAllTimers();
jest.useRealTimers();

Copilot uses AI. Check for mistakes.

expect(document.activeElement).toBe(frequencyEditorInputElement);
});
});
});

describe('firstDayOfWeek', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const CLASSES = {
form: 'dx-scheduler-form',
icon: 'dx-icon',
hidden: 'dx-hidden',
fieldItemContent: 'dx-field-item-content',

groupWithIcon: 'dx-scheduler-form-group-with-icon',
formIcon: 'dx-scheduler-form-icon',
Expand Down Expand Up @@ -858,6 +859,8 @@ export class AppointmentForm {
}

showMainGroup(): void {
this._popup.updateToolbarForMainGroup();

const currentHeight = this.dxPopup.option('height') as string | number | undefined;
const editingConfig = this.scheduler.getEditingConfig();
const configuredHeight = editingConfig?.popup?.height ?? 'auto';
Expand All @@ -866,28 +869,40 @@ export class AppointmentForm {
this.dxPopup.option('height', configuredHeight);
}

this._$mainGroup?.removeClass(CLASSES.mainHidden);
this._$mainGroup?.removeAttr('tabindex');
this._$recurrenceGroup?.addClass(CLASSES.recurrenceHidden);
this._$recurrenceGroup?.attr('tabindex', '-1');
if (this._$mainGroup) {
this._$mainGroup.removeClass(CLASSES.mainHidden);
this._$mainGroup.removeAttr('inert');

this._popup.updateToolbarForMainGroup();
this.focusFirstFocusableInGroup(this._$mainGroup);
}

if (this._$recurrenceGroup) {
this._$recurrenceGroup.addClass(CLASSES.recurrenceHidden);
this._$recurrenceGroup.attr('inert', true);
}
}

showRecurrenceGroup(): void {
this._popup.updateToolbarForRecurrenceGroup();

const currentHeight = this.dxPopup.option('height') as string | number | undefined;

if (currentHeight === 'auto' || currentHeight === undefined) {
const overlayHeight = this.dxPopup.$overlayContent().get(0).clientHeight;
this.dxPopup.option('height', overlayHeight);
}

this._$mainGroup?.addClass(CLASSES.mainHidden);
this._$mainGroup?.attr('tabindex', '-1');
this._$recurrenceGroup?.removeClass(CLASSES.recurrenceHidden);
this._$recurrenceGroup?.removeAttr('tabindex');
if (this._$mainGroup) {
this._$mainGroup.addClass(CLASSES.mainHidden);
this._$mainGroup.attr('inert', true);
}

if (this._$recurrenceGroup) {
this._$recurrenceGroup.removeClass(CLASSES.recurrenceHidden);
this._$recurrenceGroup.removeAttr('inert');

this._popup.updateToolbarForRecurrenceGroup();
this.focusFirstFocusableInGroup(this._$recurrenceGroup);
}
}

saveRecurrenceValue(): void {
Expand Down Expand Up @@ -1006,4 +1021,9 @@ export class AppointmentForm {
this.dxForm.itemOption(endTimeItemName, 'visible', visible);
this.dxForm.endUpdate();
}

private focusFirstFocusableInGroup($group: dxElementWrapper): void {
const focusTarget = $group.find(`.${CLASSES.fieldItemContent} [tabindex]`).first().get(0) as HTMLElement;
focusTarget?.focus({ preventScroll: true });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const CLASSES = {
recurrenceGroup: 'dx-scheduler-form-recurrence-group',
recurrenceHidden: 'dx-scheduler-form-recurrence-group-hidden',

recurrenceStartDateEditor: 'dx-scheduler-form-recurrence-start-date-editor',
frequencyEditor: 'dx-scheduler-form-recurrence-frequency-editor',
byMonthEditor: 'dx-scheduler-form-recurrence-by-month-editor',
dayOfMonthEditor: 'dx-scheduler-form-day-of-month-editor',
Expand Down Expand Up @@ -216,6 +217,7 @@ export class RecurrenceForm {
getStartDateCommonConfig(this.scheduler.getFirstDayOfWeek()),
{
name: EDITOR_NAMES.recurrenceStartDateEditor,
cssClass: CLASSES.recurrenceStartDateEditor,
label: {
text: messageLocalization.format('dxScheduler-editorLabelStartDate'),
},
Expand Down Expand Up @@ -269,6 +271,10 @@ export class RecurrenceForm {
}

private createRecurrenceRuleGroup(): GroupItem {
// Change of frequency editor's value causes rerender of the recurrencePatternGroup.
// To prevent focus loss in this editor, we use this flag.
let needRestoreFrequencyEditorFocus = false;
Copy link
Contributor Author

@Tucchhaa Tucchhaa Jan 16, 2026

Choose a reason for hiding this comment

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

The reason of focus loss is because recurrencePatternGroup group gets rerendered when updateDayEditorsVisibility() is called.

Another solution is to wrap weekGroup, monthGroup and yearGroup into different group here.

I have decided to use this flag, because it's used in very narrow scope and because form items already have very deep nesting (docs).

But if you have any objections, let's discuss it :)


return {
itemType: 'group',
name: GROUP_NAMES.recurrenceRuleRepeatGroup,
Expand Down Expand Up @@ -314,6 +320,13 @@ export class RecurrenceForm {
displayExpr: 'text',
onContentReady: (e): void => {
e.component.option('value', this.recurrenceRule.frequency);

if (needRestoreFrequencyEditorFocus) {
setTimeout(() => {
e.component.focus();
needRestoreFrequencyEditorFocus = false;
});
}
},
onValueChanged: (e): void => {
const previousValue = this.recurrenceRule.frequency;
Expand All @@ -322,6 +335,10 @@ export class RecurrenceForm {
return;
}

if (e.event) {
needRestoreFrequencyEditorFocus = true;
}

this.recurrenceRule.frequency = e.value;
this.updateDayEditorsVisibility();
},
Expand Down
7 changes: 6 additions & 1 deletion packages/testcafe-models/scheduler/appointment/popup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ export const SELECTORS = {
repeatEditor: `.dx-scheduler-form-repeat-editor .dx-selectbox.dx-widget`,
descriptionEditor: `.dx-scheduler-form-description-editor .dx-textarea.dx-widget`,
recurrenceGroup: '.dx-scheduler-form-recurrence-group',
backButton: `.dx-button.dx-widget[aria-label="arrowleft"]`,
recurrenceStartDateInput: '.dx-scheduler-form-recurrence-start-date-editor input[type="text"]',
recurrenceFrequencyEditor: '.dx-scheduler-form-recurrence-frequency-editor .dx-selectbox.dx-widget',
repeatEditorButton: '.dx-scheduler-form-repeat-editor .dx-button-has-icon',
recurrenceSettingsButton: '.dx-scheduler-form-recurrence-settings-button',
repeatEveryInput: '.dx-scheduler-form-recurrence-settings-group [type="text"]',
Expand Down Expand Up @@ -63,9 +66,11 @@ export default class AppointmentPopup {

descriptionEditor: TextArea = new TextArea(this.contentElement.find(SELECTORS.descriptionEditor));

// Recurrence form elements
recurrence = {
backButton: Selector(SELECTORS.backButton),
group: Selector(SELECTORS.recurrenceGroup),
startDateInput: Selector(SELECTORS.recurrenceStartDateInput),
frequencyEditor: Selector(SELECTORS.recurrenceFrequencyEditor),
settingsButton: Selector(SELECTORS.recurrenceSettingsButton),
repeatEditorButton: Selector(SELECTORS.repeatEditorButton),
repeatEveryInput: Selector(SELECTORS.repeatEveryInput),
Expand Down
Loading