Skip to content

Commit f34ec6d

Browse files
authored
Scheduler: fix aria-label for radio buttons in recurrence editor (NtPNOSbH) (DevExpress#29947)
Co-authored-by: Vladimir Bushmanov <[email protected]>
1 parent 2a5d909 commit f34ec6d

File tree

4 files changed

+129
-41
lines changed

4 files changed

+129
-41
lines changed

e2e/testcafe-devextreme/tests/scheduler/common/a11y/editing.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import Scheduler from 'devextreme-testcafe-models/scheduler';
2+
import { createWidget } from '../../../../helpers/createWidget';
3+
import url from '../../../../helpers/getPageUrl';
4+
import { a11yCheck } from '../../../../helpers/accessibility/utils';
5+
6+
fixture.disablePageReloads`a11y - popup`
7+
.page(url(__dirname, '../../../container.html'));
8+
9+
const checkOptions = {
10+
rules: {
11+
'color-contrast': { enabled: false },
12+
},
13+
};
14+
15+
test('Scheduler edit appointment is accessible', async (t) => {
16+
const scheduler = new Scheduler('#container');
17+
18+
await t.doubleClick(scheduler.getAppointmentByIndex(0).element());
19+
await t.expect(scheduler.appointmentPopup.isVisible()).ok();
20+
21+
await a11yCheck(t, checkOptions, '#container');
22+
}).before(async () => {
23+
await createWidget('dxScheduler', {
24+
timeZone: 'America/Los_Angeles',
25+
dataSource: [{
26+
text: 'Install New Router in Dev Room',
27+
startDate: new Date('2021-03-29T21:30:00.000Z'),
28+
endDate: new Date('2021-03-29T22:30:00.000Z'),
29+
recurrenceRule: 'FREQ=DAILY',
30+
}],
31+
recurrenceEditMode: 'series',
32+
currentView: 'week',
33+
currentDate: new Date(2021, 2, 28),
34+
});
35+
});
36+
37+
test('Scheduler recurrence editor repeat end accessible', async (t) => {
38+
const scheduler = new Scheduler('#container');
39+
const getItem = (index: number) => scheduler
40+
.appointmentPopup
41+
.getEndRepeatRadioButton(index);
42+
const getAriaLabel = (index: number) => getItem(index)
43+
.getAttribute('aria-label');
44+
45+
await t.doubleClick(scheduler.getAppointmentByIndex(0).element());
46+
await t.expect(scheduler.appointmentPopup.isVisible()).ok();
47+
48+
await t
49+
.expect(getAriaLabel(1))
50+
.eql('On 22 May 2025')
51+
.expect(getAriaLabel(2))
52+
.eql('After');
53+
await t
54+
.click(getItem(0))
55+
.expect(getAriaLabel(1))
56+
.eql('On')
57+
.expect(getAriaLabel(2))
58+
.eql('After');
59+
await t
60+
.click(getItem(2))
61+
.expect(getAriaLabel(1))
62+
.eql('On')
63+
.expect(getAriaLabel(2))
64+
.eql('After 1 occurrence(s)');
65+
await t
66+
.click(getItem(1))
67+
.typeText(scheduler.appointmentPopup.repeatUntilElement, '2026')
68+
.click(getItem(1)) // unfocus input
69+
.expect(getAriaLabel(1))
70+
.eql('On 22 May 2026')
71+
.expect(getAriaLabel(2))
72+
.eql('After');
73+
await t
74+
.click(getItem(2))
75+
.typeText(scheduler.appointmentPopup.repeatCountElement, '3')
76+
.click(getItem(2)) // unfocus input
77+
.expect(getAriaLabel(1))
78+
.eql('On')
79+
.expect(getAriaLabel(2))
80+
.eql('After 13 occurrence(s)');
81+
}).before(async () => {
82+
await createWidget('dxScheduler', {
83+
timeZone: 'America/Los_Angeles',
84+
dataSource: [{
85+
text: 'Install New Router in Dev Room',
86+
startDate: new Date('2021-03-29T21:30:00.000Z'),
87+
endDate: new Date('2021-03-29T22:30:00.000Z'),
88+
recurrenceRule: 'FREQ=DAILY;UNTIL=20250522T215959Z',
89+
}],
90+
recurrenceEditMode: 'series',
91+
currentView: 'week',
92+
currentDate: new Date('2021-03-29T21:30:00.000Z'),
93+
});
94+
});

packages/devextreme/js/__internal/scheduler/m_recurrence_editor.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ class RecurrenceEditor extends Editor {
191191

192192
this._prepareEditors();
193193
this._renderEditors(this._$container);
194+
this._updateRepeatInputAriaLabel();
194195
}
195196

196197
getEditorByField(fieldName) {
@@ -540,6 +541,7 @@ class RecurrenceEditor extends Editor {
540541
}
541542

542543
this._changeEditorValue();
544+
this._updateRepeatInputAriaLabel();
543545
}
544546

545547
_changeRepeatEndInputsVisibility(value = this._recurrenceRule.getRepeatEndRule()) {
@@ -580,22 +582,31 @@ class RecurrenceEditor extends Editor {
580582
};
581583
}
582584

585+
_updateRepeatInputAriaLabel(): void {
586+
const radioButtons = this.getEditorByField('repeatEnd').itemElements();
587+
const untilLabel = messageLocalization.format('dxScheduler-recurrenceOn');
588+
const untilValue = this._recurrenceForm.getEditor('until').option('value');
589+
const untilValueFormat = `${dateLocalization.format(untilValue, 'd')} ${dateLocalization.format(untilValue, 'monthAndYear')}`;
590+
const isUntilVisible = this._recurrenceForm.itemOption('until').visible;
591+
592+
const countLabel = messageLocalization.format('dxScheduler-recurrenceAfter');
593+
const countPostfix = messageLocalization.format('dxScheduler-recurrenceRepeatCount');
594+
const countValue = this._recurrenceForm.getEditor('count').option('value');
595+
const isCountVisible = this._recurrenceForm.itemOption('count').visible;
596+
597+
radioButtons[1].setAttribute('aria-label', isUntilVisible ? `${untilLabel} ${untilValueFormat}` : untilLabel);
598+
radioButtons[2].setAttribute('aria-label', isCountVisible ? `${countLabel} ${countValue} ${countPostfix}` : countLabel);
599+
}
600+
583601
_repeatCountValueChangeHandler(args) {
584602
if (this._recurrenceRule.getRepeatEndRule() === 'count') {
585603
const { value } = args;
586604
this._recurrenceRule.makeRule('count', value);
587605
this._changeEditorValue();
606+
this._updateRepeatInputAriaLabel();
588607
}
589608
}
590609

591-
_formatUntilDate(date) {
592-
if (this._recurrenceRule.getRules().until && dateUtils.sameDate(this._recurrenceRule.getRules().until, date)) {
593-
return date;
594-
}
595-
596-
return dateUtils.setToDayEnd(date);
597-
}
598-
599610
_getRepeatUntilEditorOptions() {
600611
const until = this._getUntilValue();
601612

@@ -620,6 +631,15 @@ class RecurrenceEditor extends Editor {
620631
};
621632
}
622633

634+
_formatUntilDate(date: Date): Date {
635+
const untilDate = this._recurrenceRule.getRules().until;
636+
const isSameDate = dateUtils.sameDate(untilDate, date);
637+
638+
return untilDate && isSameDate
639+
? date
640+
: dateUtils.setToDayEnd(date);
641+
}
642+
623643
_repeatUntilValueChangeHandler(args) {
624644
if (this._recurrenceRule.getRepeatEndRule() === 'until') {
625645
const dateInTimeZone = this._formatUntilDate(new Date(args.value));
@@ -634,6 +654,7 @@ class RecurrenceEditor extends Editor {
634654

635655
this._recurrenceRule.makeRule('until', dateInLocaleTimeZone);
636656
this._changeEditorValue();
657+
this._updateRepeatInputAriaLabel();
637658
}
638659
}
639660

packages/testcafe-models/scheduler/appointment/popup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ export const SELECTORS = {
3434
endDateTimeZoneInput: `.${CLASS.endDateTimeZoneEditor} .${CLASS.textEditorInput}`,
3535
allDaySwitch: `.${CLASS.allDaySwitch} .${CLASS.switch}`,
3636
recurrenceSwitch: `.${CLASS.recurrenceSwitch} .${CLASS.switch}`,
37+
repeatUntilInput: '.dx-recurrence-datebox-until-date input[type="text"]',
38+
repeatCountInput: '.dx-recurrence-numberbox-repeat-count input[type="text"]',
3739
};
3840

3941
export default class AppointmentPopup {
@@ -75,6 +77,10 @@ export default class AppointmentPopup {
7577

7678
fullScreen = this.wrapper.find(`.${CLASS.overlayWrapper} .${CLASS.fullScreen}`).exists;
7779

80+
repeatUntilElement = this.wrapper.find(SELECTORS.repeatUntilInput);
81+
82+
repeatCountElement = this.wrapper.find(SELECTORS.repeatCountInput);
83+
7884
constructor(private readonly scheduler: Selector) {
7985
}
8086

0 commit comments

Comments
 (0)