diff --git a/e2e/testcafe-devextreme/tests/scheduler/a11y/appointment.ts b/e2e/testcafe-devextreme/tests/scheduler/a11y/appointment.ts index ac1ee45a8e5a..92cc200deae6 100644 --- a/e2e/testcafe-devextreme/tests/scheduler/a11y/appointment.ts +++ b/e2e/testcafe-devextreme/tests/scheduler/a11y/appointment.ts @@ -148,3 +148,86 @@ fixture.disablePageReloads`a11y - appointment` }); }); }); + +test('appointments & collector buttons can be navigated', async (t) => { + const scheduler = new Scheduler('#container'); + + // forward + await t.click(scheduler.workSpace); + await t.pressKey('tab'); + await t.expect( + scheduler.getAppointment('App 1').element.focused, + ).ok(); + + await t.pressKey('tab'); + await t.expect( + scheduler.collectors.get(0).isFocused, + ).ok(); + + await t.pressKey('tab'); + await t.expect( + scheduler.getAppointment('App 2').element.focused, + ).ok(); + + await t.pressKey('tab'); + await t.expect( + scheduler.getAppointment('App 4').element.focused, + ).ok(); + + // backward + + await t.pressKey('shift+tab'); + await t.expect( + scheduler.getAppointment('App 2').element.focused, + ).ok(); + + await t.pressKey('shift+tab'); + await t.expect( + scheduler.collectors.get(0).isFocused, + ).ok(); + + await t.pressKey('shift+tab'); + await t.expect( + scheduler.getAppointment('App 1').element.focused, + ).ok(); + + // open list + await t.pressKey('tab'); + await t.expect( + scheduler.collectors.get(0).isFocused, + ).ok(); + + await t.pressKey('enter'); + await t.expect( + scheduler.appointmentTooltip.element.count, + ).eql(1); +}).before(async () => { + await createWidget('dxScheduler', { + dataSource: [ + { + text: 'App 1', + startDate: new Date(2021, 1, 1), + endDate: new Date(2021, 1, 1), + }, + { + text: 'App 2', + startDate: new Date(2021, 1, 2), + endDate: new Date(2021, 1, 2), + }, + { + text: 'App 3', + startDate: new Date(2021, 1, 2), + endDate: new Date(2021, 1, 2), + }, + { + text: 'App 4', + startDate: new Date(2021, 1, 3), + endDate: new Date(2021, 1, 3), + }, + ], + allDayPanelMode: 'hidden', + currentView: 'month', + maxAppointmentsPerCell: 1, + currentDate: new Date(2021, 1, 1), + }); +}); diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts index 463dea01c74d..2da4eaddbc57 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -1,8 +1,10 @@ +/* eslint-disable spellcheck/spell-checker */ import { locate, move } from '@js/animation/translator'; import registerComponent from '@js/core/component_registrator'; import domAdapter from '@js/core/dom_adapter'; import { getPublicElement } from '@js/core/element'; import { data as elementData } from '@js/core/element_data'; +import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { wrapToArray } from '@js/core/utils/array'; // @ts-expect-error @@ -105,17 +107,17 @@ class SchedulerAppointments extends CollectionWidget { const parent = super._supportedKeys(); const tabHandler = function (e) { - const appointments = this._getAccessAppointments(); - const focusedAppointment = appointments.filter('.dx-state-focused'); - let index = focusedAppointment.data(APPOINTMENT_SETTINGS_KEY).sortedIndex; - const lastIndex = appointments.length - 1; + const navigatableItems = this._getNavigatableItems(); + const focusedItem = navigatableItems.filter('.dx-state-focused'); + let index = focusedItem.data(APPOINTMENT_SETTINGS_KEY).sortedIndex; + const lastIndex = navigatableItems.length - 1; if ((index > 0 && e.shiftKey) || (index < lastIndex && !e.shiftKey)) { e.preventDefault(); e.shiftKey ? index-- : index++; - const $nextAppointment = this._getAppointmentByIndex(index); + const $nextAppointment = this._getNavigatableItemByIndex(index); this._resetTabIndex($nextAppointment); // @ts-expect-error eventsEngine.trigger($nextAppointment, 'focus'); @@ -145,14 +147,20 @@ class SchedulerAppointments extends CollectionWidget { }); } - _getAppointmentByIndex(sortedIndex) { - const appointments = this._getAccessAppointments(); - - return appointments.filter((_, $item) => elementData($item, APPOINTMENT_SETTINGS_KEY).sortedIndex === sortedIndex).eq(0); + private _getNavigatableItemByIndex(sortedIndex) { + const appointments = this._getNavigatableItems(); + return appointments.filter( + // @ts-expect-error + (_, $item) => elementData($item, APPOINTMENT_SETTINGS_KEY).sortedIndex === sortedIndex, + ).eq(0); } - _getAccessAppointments() { - return (this as any)._itemElements().filter(':visible').not('.dx-state-disabled'); + private _getNavigatableItems(): dxElementWrapper { + // @ts-expect-error + const appts = this._itemElements().filter(':visible').not('.dx-state-disabled'); + // @ts-expect-error + const apptCollectors = this.$element().find('.dx-scheduler-appointment-collector'); + return appts.add(apptCollectors); } _resetTabIndex($appointment) { @@ -163,11 +171,11 @@ class SchedulerAppointments extends CollectionWidget { _moveFocus() {} _focusTarget() { - return (this as any)._itemElements(); + return (this as any)._getNavigatableItems(); } _renderFocusTarget() { - const $appointment = this._getAppointmentByIndex(0); + const $appointment = this._getNavigatableItemByIndex(0); this._resetTabIndex($appointment); } @@ -179,7 +187,7 @@ class SchedulerAppointments extends CollectionWidget { } _focusOutHandler(e) { - const $appointment = this._getAppointmentByIndex(0); + const $appointment = this._getNavigatableItemByIndex(0); this.option('focusedElement', getPublicElement($appointment)); super._focusOutHandler(e); } @@ -898,6 +906,7 @@ class SchedulerAppointments extends CollectionWidget { items: { data: [], colors: [], settings: [] }, isAllDay: !!virtualAppointment.isAllDay, buttonColor: color, + sortedIndex: appointmentSetting.sortedIndex, }; } @@ -935,6 +944,7 @@ class SchedulerAppointments extends CollectionWidget { }, items: virtualItems, buttonColor: virtualGroup.buttonColor, + sortedIndex: virtualGroup.sortedIndex, width: buttonWidth - this.option('_collectorOffset'), height: buttonHeight, onAppointmentClick: this.option('onItemClick'), diff --git a/packages/devextreme/js/__internal/scheduler/appointments/rendering_strategies/m_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/appointments/rendering_strategies/m_strategy_base.ts index 2b2fd6d80941..af2e9f97085c 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/rendering_strategies/m_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/rendering_strategies/m_strategy_base.ts @@ -463,6 +463,12 @@ class BaseRenderingStrategy { const createItem = (currentItem, index?) => { const currentIndex = index || 0; + const skipSortIndex = this._skipSortedIndex(currentIndex); + + if (skipSortIndex) { + stack.shouldShiftAfterSkip = true; + } + return { index: currentIndex, i: currentItem.i, @@ -472,7 +478,7 @@ class BaseRenderingStrategy { top: currentItem.top, bottom: currentItem.bottom, allDay: currentItem.allDay, - sortedIndex: this._skipSortedIndex(currentIndex) ? null : sortedIndex++, + sortedIndex: skipSortIndex ? stack.startSortedIndex : sortedIndex++, }; }; @@ -483,17 +489,24 @@ class BaseRenderingStrategy { stack.top = currentItem.top; stack.bottom = currentItem.bottom; stack.allDay = currentItem.allDay; + stack.startSortedIndex = stack.items[0].sortedIndex; }; - const pushItemsInResult = (items) => { - items.forEach((item) => { + const pushItemsInResult = (stack) => { + stack.items.forEach((item) => { result.push({ index: item.index, count: maxIndexInStack + 1, i: item.i, j: item.j, - sortedIndex: item.sortedIndex, + sortedIndex: stack.shouldShiftAfterSkip && !this._skipSortedIndex(item.index) + ? item.sortedIndex + 1 + : item.sortedIndex, }); }); + + if (stack.shouldShiftAfterSkip) { + sortedIndex += 1; + } }; for (i = 0; i < sortedArray.length; i++) { @@ -518,14 +531,14 @@ class BaseRenderingStrategy { stack.bottom = Math.max(stack.bottom, currentItem.bottom); stack.allDay = currentItem.allDay; } else { - pushItemsInResult(stack.items); + pushItemsInResult(stack); stack = {}; startNewStack(currentItem); maxIndexInStack = 0; } } if (stack.items) { - pushItemsInResult(stack.items); + pushItemsInResult(stack); } return result.sort((a, b) => { diff --git a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts index cff5babe95f1..19dda92227cc 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -136,12 +136,16 @@ export class CompactAppointmentsHelper { }); } - _createCompactButtonElement({ isCompact, $container, coordinates }) { + _createCompactButtonElement({ + isCompact, $container, coordinates, sortedIndex, + }) { const result = $('
') .addClass(APPOINTMENT_COLLECTOR_CLASS) .toggleClass(COMPACT_APPOINTMENT_COLLECTOR_CLASS, isCompact) .appendTo($container); + result.data('dxAppointmentSettings', { sortedIndex }); + this._setPosition(result, coordinates); return result;