diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__daily (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__daily (fluent.blue.light).png index 33a34a12c34e..8fdb5ff204d2 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__daily (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__daily (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__hourly (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__hourly (fluent.blue.light).png index 5b7d36deb385..b431e91ed2cf 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__hourly (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__hourly (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__mobile (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__mobile (fluent.blue.light).png index 5f1da00d5ad2..924784a7155a 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__mobile (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__mobile (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__monthly (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__monthly (fluent.blue.light).png index 5e118b5d5a8f..da23ddb3f1e6 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__monthly (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__monthly (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light).png index 616a5188b288..a81c27863970 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__readonly (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light).png index 9129592dfb31..1d45f179e0cd 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__weekly (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__with-icons (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__with-icons (fluent.blue.light).png index b2587c5f6847..c00a44a6db0f 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__with-icons (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__with-icons (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__yearly (fluent.blue.light).png b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__yearly (fluent.blue.light).png index 4bbdce6c7eb8..69c9e44a16e5 100644 Binary files a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__yearly (fluent.blue.light).png and b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/etalons/scheduler__appointment__recurrence-form__yearly (fluent.blue.light).png differ diff --git a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/form.visual.ts b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/form.visual.ts index 0919a84d9246..52eea7d330b7 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/form.visual.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/common/appointmentForm/form.visual.ts @@ -300,3 +300,45 @@ 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 testScreenshot( + t, + takeScreenshot, + 'scheduler__appointment__recurrence-settings-button__not-focused.png', + { element: appointmentPopup.contentElement }, + ); + + await t.hover(appointmentPopup.recurrence.settingsButton); + + 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), + }); +}); diff --git a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss index 029eee887dd5..b2a8ad71d06d 100644 --- a/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss +++ b/packages/devextreme-scss/scss/widgets/fluent/scheduler/_index.scss @@ -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, + &.dx-button-mode-text.dx-state-hover { + background-color: rgba(0, 0, 0, .08); + } } } } 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 bf83e6e98523..013e939554bb 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 @@ -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); @@ -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 () => { 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 c60347da8dae..816df5c92e08 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_form.ts @@ -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', @@ -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'; @@ -866,15 +869,22 @@ 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) { @@ -882,12 +892,17 @@ export class AppointmentForm { 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 { @@ -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 }); + } } diff --git a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_recurrence_form.ts b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_recurrence_form.ts index 08ff6ecd90f6..9eaab3a2fad6 100644 --- a/packages/devextreme/js/__internal/scheduler/appointment_popup/m_recurrence_form.ts +++ b/packages/devextreme/js/__internal/scheduler/appointment_popup/m_recurrence_form.ts @@ -269,6 +269,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; + return { itemType: 'group', name: GROUP_NAMES.recurrenceRuleRepeatGroup, @@ -314,6 +318,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; @@ -322,6 +333,10 @@ export class RecurrenceForm { return; } + if (e.event) { + needRestoreFrequencyEditorFocus = true; + } + this.recurrenceRule.frequency = e.value; this.updateDayEditorsVisibility(); },