Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ const currentDate = Date.UTC(2021, 1, 1);
const appointmentTemplate = ({ appointmentData }) => `<div>${appointmentData.text}</div>`;

['month', 'week', 'day', 'agenda'].forEach((currentView) => {
test(`appointment should have correct aria-label without description (${currentView})`, async (t) => {
test(`appointment should have correct aria-label and have description (${currentView})`, async (t) => {
const scheduler = new Scheduler('#container');
const appointment = scheduler.getAppointment('App 1');

await t
.expect(appointment.getAriaLabel())
.eql('App 1: February 1, 2021, 12:00 PM - 1:00 PM')
.expect(await appointment.hasAriaDescription())
.notOk();
.ok();

await a11yCheck(t, a11yCheckConfig, '#container');
}).before(async () => {
Expand All @@ -36,15 +36,15 @@ const appointmentTemplate = ({ appointmentData }) => `<div>${appointmentData.tex
});
});

test(`appointment with template should have correct aria-label without description (${currentView})`, async (t) => {
test(`appointment with template should have correct aria-label and have description (${currentView})`, async (t) => {
const scheduler = new Scheduler('#container');
const appointment = scheduler.getAppointment('App 1');

await t
.expect(appointment.getAriaLabel())
.eql('App 1: February 1, 2021, 12:00 PM - 1:00 PM')
.expect(await appointment.hasAriaDescription())
.notOk();
.ok();

await a11yCheck(t, a11yCheckConfig, '#container');
}).before(async () => {
Expand All @@ -65,7 +65,7 @@ const appointmentTemplate = ({ appointmentData }) => `<div>${appointmentData.tex
.expect(appointment.getAriaLabel())
.eql('App 1: February 1, 2021, 12:00 PM - 1:00 PM')
.expect(await appointment.getAriaDescription())
.eql('Group: resource1; Group 1: resource1');
.contains('Group: resource1; Group 1: resource1');

await a11yCheck(t, a11yCheckConfig, '#container');
}).before(async () => {
Expand Down Expand Up @@ -99,7 +99,7 @@ const appointmentTemplate = ({ appointmentData }) => `<div>${appointmentData.tex
.expect(appointment.getAriaLabel())
.eql('App 1: February 1, 2021, 12:00 PM - 1:00 PM')
.expect(await appointment.getAriaDescription())
.eql('Group: resource1; Group 1: resource1');
.contains('Group: resource1; Group 1: resource1');

await a11yCheck(t, a11yCheckConfig, '#container');
}).before(async () => {
Expand Down Expand Up @@ -134,7 +134,7 @@ const appointmentTemplate = ({ appointmentData }) => `<div>${appointmentData.tex
.expect(appointment.getAriaLabel())
.eql('App 1: February 1, 2021, 12:00 PM - 1:00 PM')
.expect(await appointment.getAriaDescription())
.eql('Group: resource11, resource21; Group 1: resource11; Group 2: resource21, resource22');
.contains('Group: resource11, resource21; Group 1: resource11; Group 2: resource21, resource22');

await a11yCheck(t, a11yCheckConfig, '#container');
}).before(async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ test('Scheduler should have right aria attributes after view changed', async (t)
await t.expect(scheduler.element.getAttribute('aria-label')).contains('Scheduler. Month view');
await t.expect(scheduler.getGeneralStatusContainer().textContent).contains('Scheduler. Month view');

await t.expect(scheduler.element.getAttribute('role')).eql('group');
await t.expect(scheduler.element.getAttribute('role')).eql('application');

await scheduler.option('currentView', 'week');

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface AppointmentModel<T = HTMLDivElement> {
getGeometry: () => Position;
getColor: (view: string) => string | undefined;
getSnapshot: () => object;
isFocused: () => boolean;
}

const getColor = (appointment: HTMLDivElement): string => appointment.style.backgroundColor;
Expand Down Expand Up @@ -60,4 +61,5 @@ export const createAppointmentModel = <T extends HTMLDivElement | null>(
date: getDisplayDate(element),
...getGeometry(element),
}),
isFocused: () => element?.classList.contains('dx-state-focused') ?? false,
});
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import {
afterEach, describe, expect, it, jest,
} from '@jest/globals';
import $ from '@js/core/renderer';

import { createScheduler } from './__mock__/create_scheduler';
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';

describe('Appointments', () => {
afterEach(() => {
const $scheduler = $('.dx-scheduler');
// @ts-expect-error
$scheduler.dxScheduler('dispose');
document.body.innerHTML = '';
jest.useRealTimers();
});

it('All-day appointment should not be resizable if current view is "day"', async () => {
setupSchedulerTestEnvironment();
const { POM } = await createScheduler({
Expand Down Expand Up @@ -81,4 +87,112 @@ describe('Appointments', () => {
expect(tooltipTitleElement?.textContent?.trim()).toBe('(No subject)');
}
});

it('should have aria-describedby attr', async () => {
setupSchedulerTestEnvironment();
const { POM } = await createScheduler({
dataSource: [{
text: 'Appointment 1',
startDate: new Date(2015, 1, 9, 8),
endDate: new Date(2015, 1, 9, 9),
}],
currentView: 'day',
currentDate: new Date(2015, 1, 9, 8),
});

const appointment = POM.getAppointment();
expect(appointment.element?.hasAttribute('aria-describedby')).toBe(true);
});

describe('Keyboard Navigation', () => {
const dataSource = [
{
text: 'Appointment 1',
startDate: new Date(2015, 1, 9, 8),
endDate: new Date(2015, 1, 9, 9),
},
{
text: 'Appointment 2',
startDate: new Date(2015, 1, 9, 10),
endDate: new Date(2015, 1, 9, 11),
},
{
text: 'Appointment 3',
startDate: new Date(2015, 1, 9, 12),
endDate: new Date(2015, 1, 9, 13),
},
];

it('should focus first appointment on Home', async () => {
setupSchedulerTestEnvironment();
const { POM, keydown } = await createScheduler({
dataSource,
currentView: 'day',
currentDate: new Date(2015, 1, 9),
});

const appointments = POM.getAppointments();
const firstAppointment = appointments[0];
const lastAppointment = appointments[2];

lastAppointment.element.focus();
keydown(lastAppointment.element, 'Home');

expect(firstAppointment.isFocused()).toBe(true);
expect(lastAppointment.isFocused()).toBe(false);
});

it('should focus last appointment on End', async () => {
setupSchedulerTestEnvironment();
const { POM, keydown } = await createScheduler({
dataSource,
currentView: 'day',
currentDate: new Date(2015, 1, 9),
});

const appointments = POM.getAppointments();
const firstAppointment = appointments[0];
const lastAppointment = appointments[2];

firstAppointment.element.focus();
keydown(firstAppointment.element, 'End');

expect(firstAppointment.isFocused()).toBe(false);
expect(lastAppointment.isFocused()).toBe(true);
});

it('should not change focus when Home is pressed on the first appointment', async () => {
setupSchedulerTestEnvironment();
const { POM, keydown } = await createScheduler({
dataSource,
currentView: 'day',
currentDate: new Date(2015, 1, 9),
});

const appointments = POM.getAppointments();
const firstAppointment = appointments[0];

firstAppointment.element.focus();
keydown(firstAppointment.element, 'Home');

expect(firstAppointment.isFocused()).toBe(true);
});

it('should not change focus when End is pressed on the last appointment', async () => {
setupSchedulerTestEnvironment();
const { POM, keydown } = await createScheduler({
dataSource,
currentView: 'day',
currentDate: new Date(2015, 1, 9),
});

const appointments = POM.getAppointments();
const lastAppointment = appointments[2];

lastAppointment.element.focus();
keydown(lastAppointment.element, 'End');

expect(lastAppointment.isFocused()).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -193,14 +193,16 @@ export class Appointment extends DOMComponent<AppointmentProperties> {
// eslint-disable-next-line no-void
void getAriaDescription(this.option())
.then((text) => {
if (text) {
const id = `dx-${new Guid()}`;
const $description = $element.find(`.${APPOINTMENT_CONTENT_CLASSES.ARIA_DESCRIPTION}`);

if ($description) {
$element.attr('aria-describedby', id);
$description.text(text).attr('id', id);
}
if (!text) {
return;
}

const id = `dx-${new Guid()}`;
const $description = $element.find(`.${APPOINTMENT_CONTENT_CLASSES.ARIA_DESCRIPTION}`);

if ($description) {
$element.attr('aria-describedby', id);
$description.text(text).attr('id', id);
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('Appointment text utils', () => {
expect(await getAriaDescription({
...options,
groupTexts: [],
})).toBe('Assignee: Samantha Bright');
})).toContain('Assignee: Samantha Bright');
});

it('should return text with multiple resources', async () => {
Expand All @@ -94,7 +94,7 @@ describe('Appointment text utils', () => {
expect(await getAriaDescription({
...options,
groupTexts: [],
})).toBe('Assignee: Samantha Bright, John Heart; Room: Room 1');
})).toContain('Assignee: Samantha Bright, John Heart; Room: Room 1');
});

it('should return text with group', async () => {
Expand All @@ -103,7 +103,7 @@ describe('Appointment text utils', () => {
...options,
groupIndex: 0,
groupTexts: ['Samantha Bright'],
})).toBe('Group: Samantha Bright');
})).toContain('Group: Samantha Bright');
});

it('should return text with multiple groups and resources', async () => {
Expand All @@ -115,7 +115,7 @@ describe('Appointment text utils', () => {
...options,
groupIndex: 1,
groupTexts: ['Samantha Bright', 'Room 1'],
})).toBe('Group: Samantha Bright, Room 1; Assignee: Samantha Bright; Room: Room 1, Room 2');
})).toContain('Group: Samantha Bright, Room 1; Assignee: Samantha Bright; Room: Room 1, Room 2');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const getAriaDescription = async (options: AppointmentProperties): Promis
const texts = [
getGroupText(options),
...resources,
messageLocalization.format('dxScheduler-appointmentAriaLabel-hotkeys'),
].filter(Boolean);

return texts.join('; ');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ export class AppointmentsKeyboardNavigation {
escape: this.escHandler.bind(this),
del: this.delHandler.bind(this),
tab: this.tabHandler.bind(this),
home: this.homeHandler.bind(this),
end: this.endHandler.bind(this),
};
}

Expand Down Expand Up @@ -87,8 +89,7 @@ export class AppointmentsKeyboardNavigation {
$nextAppointment = this.getFocusableItemBySortedIndex(index);
}

this.resetTabIndex($nextAppointment);
eventsEngine.trigger($nextAppointment, 'focus');
this.focusItem($nextAppointment);
}
}

Expand All @@ -115,4 +116,29 @@ export class AppointmentsKeyboardNavigation {
resizableInstance._toggleResizingClass(false);
}
}

private homeHandler(): void {
const $firstItem = this.getFocusableItems().first();

if (this.$focusedItem && $firstItem.is(this.$focusedItem)) {
return;
}

this.focusItem($firstItem);
}

private endHandler(): void {
const $lastItem = this.getFocusableItems().last();

if (this.$focusedItem && $lastItem.is(this.$focusedItem)) {
return;
}

this.focusItem($lastItem);
}

private focusItem($item: dxElementWrapper): void {
this.resetTabIndex($item);
eventsEngine.trigger($item, 'focus');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -991,7 +991,7 @@ class Scheduler extends SchedulerOptionsBaseWidget {
this._a11yStatus = createA11yStatusContainer();
this._a11yStatus.prependTo(this.$element());
// @ts-expect-error
this.setAria({ role: 'group' });
this.setAria({ role: 'application' });
}

_initMarkupOnResourceLoaded() {
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/localization/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@

"dxScheduler-appointmentAriaLabel-group": "Group: {0}",
"dxScheduler-appointmentAriaLabel-recurring": "Recurring appointment",
"dxScheduler-appointmentAriaLabel-hotkeys": "Press Delete to delete this appointment. Press Home or End to quickly navigate to the first or last appointment",

"dxScheduler-appointmentListAriaLabel": "Appointment list",
"dxScheduler-newPopupTitle": "New Appointment",
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/localization/messages/bg.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@

"dxScheduler-appointmentAriaLabel-group": "Group: {0}",
"dxScheduler-appointmentAriaLabel-recurring": "Recurring appointment",
"dxScheduler-appointmentAriaLabel-hotkeys": "Press Delete to delete this appointment. Press Home or End to quickly navigate to the first or last appointment",

"dxScheduler-appointmentListAriaLabel": "Appointment list",
"dxScheduler-newPopupTitle": "New Appointment",
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/localization/messages/ca.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@

"dxScheduler-appointmentAriaLabel-group": "Group: {0}",
"dxScheduler-appointmentAriaLabel-recurring": "Recurring appointment",
"dxScheduler-appointmentAriaLabel-hotkeys": "Press Delete to delete this appointment. Press Home or End to quickly navigate to the first or last appointment",

"dxScheduler-appointmentListAriaLabel": "Appointment list",
"dxScheduler-newPopupTitle": "New Appointment",
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/localization/messages/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@

"dxScheduler-appointmentAriaLabel-group": "Group: {0}",
"dxScheduler-appointmentAriaLabel-recurring": "Recurring appointment",
"dxScheduler-appointmentAriaLabel-hotkeys": "Press Delete to delete this appointment. Press Home or End to quickly navigate to the first or last appointment",

"dxScheduler-appointmentListAriaLabel": "Appointment list",
"dxScheduler-newPopupTitle": "New Appointment",
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/localization/messages/da.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@

"dxScheduler-appointmentAriaLabel-group": "Gruppe: {0}",
"dxScheduler-appointmentAriaLabel-recurring": "Tilbagevendende aftale",
"dxScheduler-appointmentAriaLabel-hotkeys": "Press Delete to delete this appointment. Press Home or End to quickly navigate to the first or last appointment",

"dxScheduler-appointmentListAriaLabel": "Aftaleliste",
"dxScheduler-newPopupTitle": "New Appointment",
Expand Down
1 change: 1 addition & 0 deletions packages/devextreme/js/localization/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@

"dxScheduler-appointmentAriaLabel-group": "Gruppe: {0}",
"dxScheduler-appointmentAriaLabel-recurring": "Wiederkehrender Termin",
"dxScheduler-appointmentAriaLabel-hotkeys": "Press Delete to delete this appointment. Press Home or End to quickly navigate to the first or last appointment",

"dxScheduler-appointmentListAriaLabel": "Terminliste",
"dxScheduler-newPopupTitle": "Neuer Termin",
Expand Down
Loading
Loading