Skip to content
Draft
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 @@ -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
Expand Up @@ -29,11 +29,19 @@ export class SchedulerModel {
return new ToolbarModel(this.queries.getByRole('toolbar'));
}

getHeader(): HTMLElement {
return this.container.querySelector('.dx-scheduler-header') as HTMLElement;
}

getStatusContent(): string {
const statusElement = this.container.querySelector('.dx-screen-reader-only');
return statusElement?.textContent ?? '';
}

getWorkSpace(): HTMLElement {
return this.container.querySelector('.dx-scheduler-work-space') as HTMLElement;
}

getAppointment(text?: string): AppointmentModel<HTMLDivElement | null> {
if (!text) {
const appointments = this.getAppointments();
Expand Down
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,96 @@ describe('Appointments', () => {
expect(tooltipTitleElement?.textContent?.trim()).toBe('(No subject)');
}
});

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
@@ -0,0 +1,67 @@
import {
afterEach, beforeEach, describe, expect, it,
} from '@jest/globals';
import $ from '@js/core/renderer';

import fx from '../../../common/core/animation/fx';
import { createScheduler } from './__mock__/create_scheduler';
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';

const CLASSES = {
scheduler: 'dx-scheduler',
};

describe('Header', () => {
beforeEach(() => {
fx.off = true;
setupSchedulerTestEnvironment({ height: 600 });
});

afterEach(() => {
const $scheduler = $(document.querySelector(`.${CLASSES.scheduler}`));
// @ts-expect-error
$scheduler.dxScheduler('dispose');
document.body.innerHTML = '';
fx.off = false;
});

it('should not have tabIndex', async () => {
const { POM } = await createScheduler({
dataSource: [],
currentView: 'day',
currentDate: new Date(2021, 4, 24),
});

expect(POM.getHeader().hasAttribute('tabindex')).toBeFalsy();
});

it('should not have tabIndex after option change', async () => {
const { scheduler, POM } = await createScheduler({
dataSource: [],
currentView: 'day',
currentDate: new Date(2021, 4, 24),
});

scheduler.option('tabIndex', 0);

expect(POM.getHeader().hasAttribute('tabindex')).toBeFalsy();
});

describe('Toolbar', () => {
it('should have viewSwitcher with locateInMenu: "auto" by default', async () => {
setupSchedulerTestEnvironment();
const { scheduler } = await createScheduler({
dataSource: [],
currentView: 'day',
currentDate: new Date(2021, 4, 24),
});

const toolbarItems = scheduler.option('toolbar.items') as any[];
const viewSwitcherItem = toolbarItems.find((item: any) => item.name === 'viewSwitcher');

expect(viewSwitcherItem).toBeDefined();
expect(viewSwitcherItem.location).toBe('after');
expect(viewSwitcherItem.locateInMenu).toBe('auto');
});
});
});

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';

const CLASSES = {
scheduler: 'dx-scheduler',
workSpace: 'dx-scheduler-work-space',
};

describe('Workspace Recalculation with Async Templates (T661335)', () => {
describe('Workspace', () => {
beforeEach(() => {
fx.off = true;
setupSchedulerTestEnvironment({ height: 600 });
Expand All @@ -28,7 +27,7 @@ describe('Workspace Recalculation with Async Templates (T661335)', () => {
});

it('should not duplicate workspace elements when resources are loaded asynchronously (T661335)', async () => {
const { scheduler, container } = await createScheduler({
const { scheduler, container, POM } = await createScheduler({
templatesRenderAsynchronously: true,
currentView: 'day',
views: ['day'],
Expand Down Expand Up @@ -68,12 +67,34 @@ describe('Workspace Recalculation with Async Templates (T661335)', () => {

await new Promise((r) => { setTimeout(r); });

const $workSpaces = $(container).find(`.${CLASSES.workSpace}`);
const $workSpaces = $(POM.getWorkSpace());
const $groupHeader = $(container).find('.dx-scheduler-group-header');

expect($workSpaces.length).toBe(1);

expect($groupHeader.length).toBeGreaterThan(0);
expect($groupHeader.text()).toContain('Room 1');
});

it('should not have tabIndex attr', async () => {
const { POM } = await createScheduler({
currentView: 'day',
views: ['day'],
currentDate: new Date(2017, 4, 25),
});

expect(POM.getWorkSpace().hasAttribute('tabindex')).toBeFalsy();
});

it('should not have tabIndex attr after option change', async () => {
const { scheduler, POM } = await createScheduler({
currentView: 'day',
views: ['day'],
currentDate: new Date(2017, 4, 25),
});

scheduler.option('tabIndex', 1);

expect(POM.getWorkSpace().hasAttribute('tabindex')).toBeFalsy();
});
});
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');
}
}
10 changes: 7 additions & 3 deletions packages/devextreme/js/__internal/scheduler/m_scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,10 @@ class Scheduler extends SchedulerOptionsBaseWidget {
}
break;
case 'tabIndex':
this._appointments.option(name, value);
// @ts-expect-error
super._optionChanged(args);
break;
case 'focusStateEnabled':
this._updateOption('header', name, value);
this._updateOption('workSpace', name, value);
Expand Down Expand Up @@ -991,7 +995,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 Expand Up @@ -1227,7 +1231,7 @@ class Scheduler extends SchedulerOptionsBaseWidget {
max: this.getViewOption('max'),
indicatorTime: this.option('indicatorTime'),
startViewDate: this.getStartViewDate(),
tabIndex: this.option('tabIndex'),
tabIndex: undefined,
focusStateEnabled: this.option('focusStateEnabled'),
useDropDownViewSwitcher: this.option('useDropDownViewSwitcher'),
firstDayOfWeek: this.getFirstDayOfWeek(),
Expand Down Expand Up @@ -1356,7 +1360,7 @@ class Scheduler extends SchedulerOptionsBaseWidget {
startDayHour: this.option('startDayHour'),
endDayHour: this.option('endDayHour'),
viewOffset: this.getViewOffsetMs(),
tabIndex: this.option('tabIndex'),
tabIndex: undefined,
accessKey: this.option('accessKey'),
focusStateEnabled: this.option('focusStateEnabled'),
cellDuration: this.option('cellDuration'),
Expand Down
Loading