Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import type { Properties } from '@js/ui/scheduler';
import { fireEvent } from '@testing-library/dom';

import { createScheduler as baseCreateScheduler } from './__mock__/create_scheduler';
import { setupSchedulerTestEnvironment } from './__mock__/m_mock_scheduler';
Expand Down Expand Up @@ -391,6 +392,7 @@ describe('New Appointments', () => {
});
});

<<<<<<< HEAD
describe('onAppointmentClick', () => {
it('should call onAppointmentClick callback', async () => {
const onAppointmentClick = jest.fn();
Expand Down Expand Up @@ -756,4 +758,88 @@ describe('New Appointments', () => {
expect(appointment.text).toBe('Updated Appointment');
});
});

describe('Keyboard navigation', () => {
it('should delete appointment by delete key', async () => {
const { POM } = await createScheduler({
dataSource: [{
startDate: new Date(2015, 1, 9, 8),
endDate: new Date(2015, 1, 9, 9),
}],
currentDate: new Date(2015, 1, 9, 8),
});

const appointment = POM.getAppointments()[0];
appointment.element.focus();
fireEvent.keyDown(appointment.element, { key: 'Delete' });
await new Promise(process.nextTick);

expect(POM.getAppointments().length).toBe(0);
});
Comment thread
bit-byte0 marked this conversation as resolved.

it('should delete recurring appointment occurrence by delete key', async () => {
const { POM } = await createScheduler({
dataSource: [{
startDate: new Date(2015, 1, 9, 8),
endDate: new Date(2015, 1, 9, 9),
recurrenceRule: 'FREQ=DAILY;COUNT=3',
}],
currentDate: new Date(2015, 1, 9),
currentView: 'week',
recurrenceEditMode: 'occurrence',
});

expect(POM.getAppointments().length).toBe(3);

const appointment = POM.getAppointments()[0];
appointment.element.focus();
fireEvent.keyDown(appointment.element, { key: 'Delete' });
await new Promise(process.nextTick);

expect(POM.getAppointments().length).toBe(2);
});
Comment thread
bit-byte0 marked this conversation as resolved.

it.each([
{ editing: true },
{ editing: { allowDeleting: true } },
{ editing: { allowDeleting: true, allowUpdating: false } },
])('should delete appointment when editing=$editing', async ({ editing }) => {
const { POM } = await createScheduler({
dataSource: [{
startDate: new Date(2015, 1, 9, 8),
endDate: new Date(2015, 1, 9, 9),
}],
currentDate: new Date(2015, 1, 9, 8),
editing,
});

const appointment = POM.getAppointments()[0];
appointment.element.focus();
fireEvent.keyDown(appointment.element, { key: 'Delete' });
await new Promise(process.nextTick);

expect(POM.getAppointments().length).toBe(0);
});

it.each([
{ editing: { allowDeleting: false } },
{ editing: false },
])('should NOT delete appointment when editing=$editing', async ({ editing }) => {
const { POM } = await createScheduler({
dataSource: [{
startDate: new Date(2015, 1, 9, 8),
endDate: new Date(2015, 1, 9, 9),
}],
currentDate: new Date(2015, 1, 9, 8),
editing,
});

const appointment = POM.getAppointments()[0];
appointment.element.focus();
fireEvent.keyDown(appointment.element, { key: 'Delete' });
await new Promise(process.nextTick);

expect(POM.getAppointments().length).toBe(1);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ export interface BaseAppointmentViewProperties
export class BaseAppointmentView<
TProperties extends BaseAppointmentViewProperties = BaseAppointmentViewProperties,
> extends ViewItem<TProperties> {
get targetedAppointmentData(): TargetedAppointment {
public get targetedAppointmentData(): TargetedAppointment {
return this.option().targetedAppointmentData;
}

get appointmentData(): SafeAppointment {
public get appointmentData(): SafeAppointment {
return this.option().appointmentData;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ import { focus } from '@ts/events/m_short';

import { getRawAppointmentGroupValues } from '../utils/resource_manager/appointment_groups_utils';
import type { SortedEntity } from '../view_model/types';
import type { BaseAppointmentView } from './appointment/base_appointment';
import { AppointmentCollector } from './appointment_collector';
import type { Appointments } from './appointments';
import type { ViewItem } from './view_item';

interface AppointmentsFocusControllerHandlers {
onAppointmentEnterKeyDown: (appointmentView: BaseAppointmentView, event: DxEvent) => void;
}

export class AppointmentsFocusController {
private focusableSortedIndex = 0;

private needRestoreFocusIndex = -1;

private get sortedAppointments(): SortedEntity[] {
return this.appointments.option().getSortedAppointments();
return this.appointments.option().getSortedItems();
}

private get isVirtualScrolling(): boolean {
Expand All @@ -25,7 +31,10 @@ export class AppointmentsFocusController {
return this.appointments.option().tabIndex;
}

constructor(private readonly appointments: Appointments) { }
constructor(
private readonly appointments: Appointments,
private readonly handlers: AppointmentsFocusControllerHandlers,
) { }

public onViewItemClick(viewItem: ViewItem): void {
this.focusViewItem(viewItem);
Expand All @@ -50,8 +59,27 @@ export class AppointmentsFocusController {
}

public onViewItemKeyDown(viewItem: ViewItem, e: KeyboardKeyDownEvent): void {
if (e.key === 'Tab') {
this.handleTabKeyDown(e, viewItem.option().sortedIndex);
switch (true) {
case e.key === 'Tab':
this.handleTabKeyDown(e, viewItem.option().sortedIndex);
break;
Comment thread
bit-byte0 marked this conversation as resolved.
case e.key === 'Delete':
this.handleDeleteKeyDown(viewItem);
break;
case e.key === 'Home':
this.handleHomeKeyDown(e);
break;
case e.key === 'End':
this.handleEndKeyDown(e);
break;
case e.key === 'Enter':
this.handleEnterKeyDown(viewItem, e);
break;
case e.key === ' ':
Comment thread
bit-byte0 marked this conversation as resolved.
this.handleEnterKeyDown(viewItem, e);
break;
default:
break;
}
}

Expand Down Expand Up @@ -89,20 +117,62 @@ export class AppointmentsFocusController {
}

e.originalEvent.preventDefault();
this.focusByItemData(nextItemData);
this.focusBySortedItem(nextItemData);
}

private handleDeleteKeyDown(viewItem: ViewItem): void {
if (viewItem instanceof AppointmentCollector) { return; }

const { allowDelete, onDeleteKeyPress } = this.appointments.option();
if (!allowDelete) { return; }

const appointmentViewItem = viewItem as BaseAppointmentView;
onDeleteKeyPress({
appointmentData: appointmentViewItem.appointmentData,
targetedAppointmentData: appointmentViewItem.targetedAppointmentData,
});
}

private handleHomeKeyDown(e: KeyboardKeyDownEvent): void {
const firstSortedItem = this.sortedAppointments[0];
if (firstSortedItem) {
e.originalEvent.preventDefault();
this.focusBySortedItem(firstSortedItem);
}
}

private handleEndKeyDown(e: KeyboardKeyDownEvent): void {
const lastSortedItem = this.sortedAppointments[this.sortedAppointments.length - 1];
if (lastSortedItem) {
e.originalEvent.preventDefault();
this.focusBySortedItem(lastSortedItem);
}
}

private handleEnterKeyDown(viewItem: ViewItem, e: KeyboardKeyDownEvent): void {
e.originalEvent.preventDefault();

if (viewItem instanceof AppointmentCollector) {
return;
}

this.handlers.onAppointmentEnterKeyDown(
viewItem as BaseAppointmentView,
e.originalEvent as DxEvent,
);
}

private focusByItemData(itemData: SortedEntity): void {
private focusBySortedItem(sortedItem: SortedEntity): void {
if (this.isVirtualScrolling) {
this.scrollToItem(itemData);
this.scrollToItem(sortedItem);
}

const viewItem = this.appointments.getViewItemBySortedIndex(itemData.sortedIndex);
const viewItem = this.appointments.getViewItemBySortedIndex(sortedItem.sortedIndex);

if (viewItem) {
this.focusViewItem(viewItem);
} else if (this.isVirtualScrolling) {
this.needRestoreFocusIndex = itemData.sortedIndex;
this.needRestoreFocusIndex = sortedItem.sortedIndex;
}
}

Expand All @@ -111,19 +181,19 @@ export class AppointmentsFocusController {
focus.trigger(viewItem?.$element());
}

private scrollToItem(itemData: SortedEntity): void {
private scrollToItem(sortedItem: SortedEntity): void {
const { getStartViewDate, getResourceManager, scrollTo } = this.appointments.option();

const date = new Date(Math.max(
getStartViewDate().getTime(),
itemData.source.startDate,
sortedItem.source.startDate,
));

const group = getRawAppointmentGroupValues(
itemData.itemData,
sortedItem.itemData,
getResourceManager().resources,
);

scrollTo(date, { group, allDay: itemData.allDay });
scrollTo(date, { group, allDay: sortedItem.allDay });
}
}
Loading
Loading