From cef2f3f872f411c97f8873c5d435de1007ba3ee4 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Mon, 2 Mar 2026 14:48:16 +0800 Subject: [PATCH 1/3] prefix --- .../appointments/m_appointment_collection.ts | 85 +++++---- .../appointments/m_appointments_kbn.ts | 162 +++++++++++------- .../js/__internal/scheduler/m_scheduler.ts | 8 +- .../view_model/appointments_layout_manager.ts | 16 +- .../generate_grid_view_model.ts | 32 +++- .../__internal/scheduler/view_model/types.ts | 10 +- .../workspaces/m_virtual_scrolling.ts | 4 +- .../scheduler/workspaces/m_work_space.ts | 8 +- 8 files changed, 213 insertions(+), 112 deletions(-) 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 f1a32bc4f6c3..ae08c810549a 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -68,7 +68,7 @@ interface ViewModelDiff { class SchedulerAppointments extends CollectionWidget { // NOTE: The key of this array is `sortedIndex` of appointment rendered in Element - renderedElementsBySortedIndex: dxElementWrapper[] = []; + $itemBySortedIndex: dxElementWrapper[] = []; _appointmentClickTimeout: any; @@ -82,17 +82,19 @@ class SchedulerAppointments extends CollectionWidget { private _kbn!: AppointmentsKeyboardNavigation; + private _focusedItemIndexBeforeRender!: number; + private _isResizing = false; public get isResizing(): boolean { return this._isResizing; } - get isAgendaView() { + get isAgendaView(): boolean { return this.invoke('isCurrentViewAgenda'); } - get isVirtualScrolling() { + get isVirtualScrolling(): boolean { return this.invoke('isVirtualScrolling'); } @@ -138,7 +140,10 @@ class SchedulerAppointments extends CollectionWidget { const parentValue = super._supportedKeys(); const kbnValue = this._kbn.getSupportedKeys(); - return extend(parentValue, kbnValue) as SupportedKeys; + return { + ...parentValue, + ...kbnValue, + }; } public getAppointmentSettings($item: dxElementWrapper): AppointmentViewModelPlain { @@ -152,8 +157,26 @@ class SchedulerAppointments extends CollectionWidget { } _renderFocusTarget() { - const $item = this._kbn.getFocusableItemBySortedIndex(0); - this._kbn.resetTabIndex($item); + this._kbn.resetTabIndex(); + } + + _cleanFocusState(): void { + this._focusedItemIndexBeforeRender = this._kbn.isNavigating + ? this._kbn.focusedItemSortIndex + : -1; + + super._cleanFocusState(); + } + + _renderFocusState(): void { + super._renderFocusState(); + + if (this._focusedItemIndexBeforeRender !== -1) { + this._kbn.focusedItemSortIndex = this._focusedItemIndexBeforeRender; + this._kbn.isNavigating = false; + this._kbn.focus(); + this._focusedItemIndexBeforeRender = -1; + } } _focusInHandler(e) { @@ -162,7 +185,7 @@ class SchedulerAppointments extends CollectionWidget { } _focusOutHandler(e) { - this._kbn.focusOutHandler(); + this._kbn.focusOutHandler(e); super._focusOutHandler(e); } @@ -192,7 +215,7 @@ class SchedulerAppointments extends CollectionWidget { value: AppointmentViewModelPlain[] = [], ): ViewModelDiff[] { const elementsInRenderOrder = previousValue - .map(({ sortedIndex }) => this.renderedElementsBySortedIndex[sortedIndex]); + .map(({ sortedIndex }) => this.$itemBySortedIndex[sortedIndex]); const diff = getViewModelDiff(previousValue, value, this.appointmentDataSource); diff .filter((item) => !isNeedToAdd(item)) @@ -206,7 +229,9 @@ class SchedulerAppointments extends CollectionWidget { _optionChanged(args) { switch (args.name) { case 'items': - (this as any)._cleanFocusState(); + console.log('render', this._kbn.focusedItemSortIndex); + + this._cleanFocusState(); if (this.isAgendaView) { this.forceRepaintAllAppointments(args.value || []); @@ -225,12 +250,11 @@ class SchedulerAppointments extends CollectionWidget { case 'allowResize': case 'allowDelete': case 'allowAllDayResize': - (this as any)._cleanFocusState(); + this._cleanFocusState(); this.forceRepaintAllAppointments(this.option('items') || []); this._attachAppointmentsEvents(); break; case 'focusedElement': - this._kbn.resetTabIndex($(args.value)); super._optionChanged(args); break; case 'focusStateEnabled': @@ -251,7 +275,7 @@ class SchedulerAppointments extends CollectionWidget { } protected forceRepaintAllAppointments(items: AppointmentViewModelPlain[]): void { - this.renderedElementsBySortedIndex = []; + this.$itemBySortedIndex = []; this._renderByFragments(($commonFragment, $allDayFragment) => { this._getAppointmentContainer(true).html(''); this._getAppointmentContainer(false).html(''); @@ -269,7 +293,7 @@ class SchedulerAppointments extends CollectionWidget { } protected repaintAppointments(diff: ViewModelDiff[]): void { - this.renderedElementsBySortedIndex = []; + this.$itemBySortedIndex = []; this._renderByFragments(($commonFragment, $allDayFragment) => { const isRepaintAll = this.isAgendaView || !diff.some((item) => item.needToAdd === undefined && item.needToRemove === undefined); @@ -303,7 +327,7 @@ class SchedulerAppointments extends CollectionWidget { if (item.element) { item.element.data(APPOINTMENT_SETTINGS_KEY, item.item); - this.renderedElementsBySortedIndex[item.item.sortedIndex] = item.element; + this.$itemBySortedIndex[item.item.sortedIndex] = item.element; } }); }); @@ -334,14 +358,14 @@ class SchedulerAppointments extends CollectionWidget { } _attachAppointmentsEvents() { - (this as any)._attachClickEvent(); - (this as any)._attachHoldEvent(); - (this as any)._attachContextMenuEvent(); - (this as any)._attachAppointmentDblClick(); + this._attachClickEvent(); + this._attachHoldEvent(); + this._attachContextMenuEvent(); + this._attachAppointmentDblClick(); - (this as any)._renderFocusState(); - (this as any)._attachFeedbackEvents(); - (this as any)._attachHoverEvents(); + this._renderFocusState(); + this._attachFeedbackEvents(); + this._attachHoverEvents(); } _clearDropDownItemsElements() { @@ -390,7 +414,8 @@ class SchedulerAppointments extends CollectionWidget { _init() { super._init(); this._kbn = new AppointmentsKeyboardNavigation(this); - (this as any).$element().addClass(COMPONENT_CLASS); + this._focusedItemIndexBeforeRender = -1; + this.$element().addClass(COMPONENT_CLASS); this._preventSingleAppointmentClick = false; } @@ -478,6 +503,7 @@ class SchedulerAppointments extends CollectionWidget { } _render() { + (window as any).y = this; super._render(); this._attachAppointmentDblClick(); } @@ -519,12 +545,13 @@ class SchedulerAppointments extends CollectionWidget { const $item = super._renderItem(index, item.itemData, container); $item.data(APPOINTMENT_SETTINGS_KEY, item); + if (item.sortedIndex !== -1) { // NOTE: fallback for integration testing - if (!this.renderedElementsBySortedIndex) { - this.renderedElementsBySortedIndex = []; + if (!this.$itemBySortedIndex) { + this.$itemBySortedIndex = []; } - this.renderedElementsBySortedIndex[item.sortedIndex] = $item; + this.$itemBySortedIndex[item.sortedIndex] = $item; } return $item; @@ -673,7 +700,7 @@ class SchedulerAppointments extends CollectionWidget { const $appointment = $(e.element); this._isResizing = true; - this._kbn.$focusedItem = $appointment; + this._kbn.focus($appointment); if (this.invoke('needRecalculateResizableArea')) { const updatedArea = this._calculateResizableArea( @@ -983,13 +1010,13 @@ class SchedulerAppointments extends CollectionWidget { allowDrag: this.option('allowDrag'), isCompact: appointment.isCompact, }); - this.renderedElementsBySortedIndex[appointment.sortedIndex] = $item; + this.$itemBySortedIndex[appointment.sortedIndex] = $item; return $item; } moveAppointmentBack(dragEvent?) { - const $appointment = this._kbn.$focusedItem; + const $appointment = this._kbn.$focusTarget(); const size = this._initialSize; const coords = this._initialCoordinates; @@ -1005,7 +1032,7 @@ class SchedulerAppointments extends CollectionWidget { } } - if ($appointment && !dragEvent) { + if ($appointment.get(0) && !dragEvent) { if (coords) { move($appointment, coords); delete this._initialSize; diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts index 56c01578c029..0184bbf322f0 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts @@ -5,18 +5,24 @@ import { getPublicElement } from '@ts/core/m_element'; import type { SupportedKeys } from '@ts/core/widget/widget'; import eventsEngine from '@ts/events/core/m_events_engine'; +import type { AppointmentViewModelPlain } from '../view_model/types'; import type SchedulerAppointments from './m_appointment_collection'; -import { getNextElement, getPrevElement } from './utils/sorted_index_utils'; + +// Case when no appointments in Viewport +// Case when focusedAppointment is outside Viewport export class AppointmentsKeyboardNavigation { private readonly _collection: SchedulerAppointments; - public $focusedItem: dxElementWrapper | null = null; + public focusedItemSortIndex = -1; + + public isNavigating = false; constructor(collection: SchedulerAppointments) { this._collection = collection; } + // TODO: make disabled appointments focusable and remove this method public getFocusableItems(): dxElementWrapper { const appts = this._collection._itemElements().not('.dx-state-disabled'); const collectors = this._collection.$element().find('.dx-scheduler-appointment-collector'); @@ -24,34 +30,34 @@ export class AppointmentsKeyboardNavigation { return appts.add(collectors); } - public getFocusableItemBySortedIndex(sortedIndex: number): dxElementWrapper { - const $items = this.getFocusableItems(); - const itemElement = $items.toArray().filter((itemElement: Element) => { - const $item = $(itemElement); - const itemData = this._collection.getAppointmentSettings($item); - return itemData.sortedIndex === sortedIndex; - }); - - return $(itemElement); - } - - public focus(): void { - if (this.$focusedItem) { - const focusedElement = getPublicElement(this.$focusedItem); + public focus($item?: dxElementWrapper): void { + const $target = $item ?? this.$focusTarget(); - this._collection.option('focusedElement', focusedElement); - eventsEngine.trigger(focusedElement, 'focus'); + if ($target.length) { + eventsEngine.trigger($target, 'focus'); } } public focusInHandler(e: DxEvent): void { - this.$focusedItem = $(e.target); - this._collection.option('focusedElement', getPublicElement(this.$focusedItem)); + const $target = $(e.target); + const itemData = this._collection.getAppointmentSettings($target); + + this.focusedItemSortIndex = itemData.sortedIndex; + this.resetTabIndex(); + this._collection.option('focusedElement', getPublicElement(e.target)); } - public focusOutHandler(): void { - const $item = this.getFocusableItemBySortedIndex(0); - this._collection.option('focusedElement', getPublicElement($item)); + public focusOutHandler(e: DxEvent): void { + const $container = this._collection._itemContainer(); + const isFocusInside = $(e.relatedTarget as Element).closest($container).length > 0; + + if (isFocusInside || this.isNavigating) { + return; + } + + this.focusedItemSortIndex = -1; + this.resetTabIndex(); + this._collection.option('focusedElement', null); } public getSupportedKeys(): SupportedKeys { @@ -64,33 +70,29 @@ export class AppointmentsKeyboardNavigation { }; } - public resetTabIndex($appointment: dxElementWrapper): void { - this.getFocusableItems().attr('tabIndex', -1); - $appointment.attr('tabIndex', this._collection.option('tabIndex')); - } + public $focusTarget(): dxElementWrapper { + const $items = this._collection.$itemBySortedIndex; - private tabHandler(e): void { - if (!this.$focusedItem) { - return; + if (!$items?.length) { + return $(); } - const $focusableItems = this.getFocusableItems(); - let index = this._collection.getAppointmentSettings(this.$focusedItem).sortedIndex; - let $nextAppointment = e.shiftKey - ? getPrevElement(index, this._collection.renderedElementsBySortedIndex) - : getNextElement(index, this._collection.renderedElementsBySortedIndex); - const lastIndex = $focusableItems.length - 1; + if (this.focusedItemSortIndex === -1) { + const $itemsPlainArray = Object.values($items); - if ($nextAppointment || (index > 0 && e.shiftKey) || (index < lastIndex && !e.shiftKey)) { - e.preventDefault(); + return this._collection.isVirtualScrolling + ? $itemsPlainArray.find(($item) => this.isItemVisibleInViewport($item)) ?? $() + : $($itemsPlainArray[0]); + } - if (!$nextAppointment) { - e.shiftKey ? index-- : index++; - $nextAppointment = this.getFocusableItemBySortedIndex(index); - } + const $item = $items[this.focusedItemSortIndex]; - this.focusItem($nextAppointment); - } + return $item || $(); + } + + public resetTabIndex(): void { + this.getFocusableItems().attr('tabIndex', -1); + this.$focusTarget().attr('tabIndex', this._collection.option('tabIndex')); } private delHandler(e: DxEvent): void { @@ -108,7 +110,7 @@ export class AppointmentsKeyboardNavigation { this._collection.moveAppointmentBack(); - const resizableInstance = (this.$focusedItem as any).dxResizable('instance'); + const resizableInstance = (this.$focusTarget() as any).dxResizable('instance'); if (resizableInstance) { resizableInstance._detachEventHandlers(); @@ -117,32 +119,76 @@ export class AppointmentsKeyboardNavigation { } } - private homeHandler(e: DxEvent): void { - e.preventDefault(); - - const $firstItem = this.getFocusableItems().first(); + private tabHandler(e: DxEvent): void { + const items = this._collection.option('getLayoutManager')().sortedItems; + const nextIndex = this.focusedItemSortIndex + (e.shiftKey ? -1 : 1); + const nextItemData = items[nextIndex]; - if (this.$focusedItem && $firstItem.is(this.$focusedItem)) { + if (!nextItemData) { return; } - this.focusItem($firstItem); + e.preventDefault(); + this.focusByItemData(nextItemData); } - private endHandler(e: DxEvent): void { + private homeHandler(e: DxEvent): void { + const items = this._collection.option('getLayoutManager')().sortedItems; + const nextItemData = items[0]; + + if (!nextItemData) { + return; + } + e.preventDefault(); + this.focusByItemData(nextItemData); + } - const $lastItem = this.getFocusableItems().last(); + private endHandler(e: DxEvent): void { + const items = this._collection.option('getLayoutManager')().sortedItems; + const nextItemData = items[items.length - 1]; - if (this.$focusedItem && $lastItem.is(this.$focusedItem)) { + if (!nextItemData) { return; } - this.focusItem($lastItem); + e.preventDefault(); + this.focusByItemData(nextItemData); + } + + private focusByItemData(itemData: AppointmentViewModelPlain): void { + if (this._collection.isVirtualScrolling) { + this.focusedItemSortIndex = itemData.sortedIndex; + this.isNavigating = true; + this.scrollToByItemData(itemData); + } + + this.focus(); + } + + private scrollToByItemData(itemData: AppointmentViewModelPlain): void { + const date = new Date(Math.max( + this._collection.option('getStartViewDate')().getTime(), + (itemData as any).source.startDate, + )); + + this._collection.option('scrollTo')( + date, + { + group: itemData.itemData, + allDay: itemData.allDay, + }, + ); } - private focusItem($item: dxElementWrapper): void { - this.resetTabIndex($item); - eventsEngine.trigger($item, 'focus'); + private isItemVisibleInViewport($item: dxElementWrapper): boolean { + const $container = this._collection.$element().closest('.dx-scrollable-container'); + const containerRect = $container.get(0).getBoundingClientRect(); + const itemRect = $item.get(0).getBoundingClientRect(); + + return (itemRect.top < containerRect.bottom + && itemRect.bottom > containerRect.top + && itemRect.left < containerRect.right + && itemRect.right > containerRect.left); } } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 73054577d215..18ac5e06c02f 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -1234,8 +1234,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { _appointmentsConfig() { const config = { getResourceManager: () => this.resourceManager, - + getLayoutManager: () => this._layoutManager, getAppointmentDataSource: () => this.appointmentDataSource, + getStartViewDate: this.getStartViewDate.bind(this), + scrollTo: this.scrollTo.bind(this), dataAccessors: this._dataAccessors, notifyScheduler: this._notifyScheduler, onItemRendered: this._getAppointmentRenderedAction(), @@ -1372,9 +1374,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { schedulerWidth: this.option('width'), allDayPanelMode: this.option('allDayPanelMode'), onSelectedCellsClick: this.showAddAppointmentPopup.bind(this), - onRenderAppointments: () => { - this._renderAppointments(); - }, + renderAppointments: () => { this._renderAppointments(); }, onShowAllDayPanel: (value) => this.option('showAllDayPanel', value), getHeaderHeight: () => utils.DOM.getHeaderHeight(this._header), onScrollEnd: () => this._appointments.updateResizableArea(), diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts index 6ea190eaa0dc..8ccc994ccf67 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts @@ -5,7 +5,7 @@ import { filterAppointments } from './filtration/filter_appointments'; import type { Occurrence } from './filtration/get_occurrences'; import { getOccurrences } from './filtration/get_occurrences'; import { generateAgendaViewModel } from './generate_view_model/generate_agenda_view_model'; -import { generateGridViewModel } from './generate_view_model/generate_grid_view_model'; +import { generateGridViewModel, sortAppointments } from './generate_view_model/generate_grid_view_model'; import type { RealSize } from './generate_view_model/steps/add_geometry/types'; import { getAgendaAppointmentInfo, getAppointmentInfo } from './get_appointment_info'; import { prepareAppointments } from './preparation/prepare_appointments'; @@ -16,6 +16,7 @@ import type { AppointmentViewModelPlain, ListEntity, MinimalAppointmentEntity, + SortedEntity, UTCDatesBeforeSplit, } from './types'; @@ -24,6 +25,12 @@ class AppointmentLayoutManager { filteredItems: ListEntity[] = []; + private _sortedItems: SortedEntity[] = []; + + public get sortedItems(): SortedEntity[] { return this._sortedItems; } + + viewModel: AppointmentViewModelPlain[] = []; + // NOTE: Here we should pass global store. But right now scheduler component is global store constructor(public schedulerStore: Scheduler) {} @@ -35,6 +42,10 @@ class AppointmentLayoutManager { this.filteredItems = filterAppointments(this.schedulerStore, this.preparedItems); } + private sortAppointments(): void { + this._sortedItems = sortAppointments(this.schedulerStore, this.filteredItems); + } + public getOccurrences( startDate: Date, endDate: Date, @@ -66,10 +77,11 @@ class AppointmentLayoutManager { })); } + this.sortAppointments(); const isSkipResizing = (appointment: ListEntity): boolean => appointment.isAllDayPanelOccupied && viewType === 'day' && this.schedulerStore.currentView.intervalCount === 1; - const viewModel = generateGridViewModel(this.schedulerStore, this.filteredItems); + const viewModel = generateGridViewModel(this.schedulerStore, this._sortedItems); const toItem = (item: AppointmentEntity): AppointmentItemViewModel => ({ itemData: item.itemData, allDay: item.isAllDayPanelOccupied, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts index b91034f26441..b91e47257dc3 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/generate_view_model/generate_grid_view_model.ts @@ -1,5 +1,5 @@ import type Scheduler from '../../m_scheduler'; -import type { AppointmentEntity, ListEntity } from '../types'; +import type { AppointmentEntity, ListEntity, SortedEntity } from '../types'; import { OptionManager } from './options/option_manager'; import { addCollector } from './steps/add_collector/add_collector'; import { addDirection } from './steps/add_direction'; @@ -16,22 +16,17 @@ import { splitByParts } from './steps/split_by_parts/split_by_parts'; import { cropByVirtualScreen } from './steps/virtual_screen_crop'; import { filterByVirtualScreen } from './steps/virtual_screen_filter'; -export const generateGridViewModel = ( +export const sortAppointments = ( schedulerStore: Scheduler, items: ListEntity[], -): AppointmentEntity[] => { +): SortedEntity[] => { const optionManager = new OptionManager(schedulerStore); const { - viewOrientation, isMonthView, - isAdaptivityEnabled, - isTimelineView, hasAllDayPanel, - isVirtualScrolling, viewOffset, compareOptions: { endDayHour }, } = optionManager.options; - const { viewDataProvider } = schedulerStore._workSpace; const step2 = maybeSplit(items, hasAllDayPanel, (entities, panelName) => { const byGroup = groupByGroupIndex(entities); @@ -57,8 +52,27 @@ export const generateGridViewModel = ( }); const step3 = addSortedIndex(step2); + + return step3; +}; + +export const generateGridViewModel = ( + schedulerStore: Scheduler, + items: SortedEntity[], +): AppointmentEntity[] => { + const optionManager = new OptionManager(schedulerStore); + const { + viewOrientation, + isMonthView, + isAdaptivityEnabled, + isTimelineView, + hasAllDayPanel, + isVirtualScrolling, + } = optionManager.options; + const { viewDataProvider } = schedulerStore._workSpace; + const step4 = filterByVirtualScreen( - step3, + items, viewDataProvider, isVirtualScrolling, ); diff --git a/packages/devextreme/js/__internal/scheduler/view_model/types.ts b/packages/devextreme/js/__internal/scheduler/view_model/types.ts index 84fc9e34853d..591d2be66ccc 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/types.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/types.ts @@ -149,14 +149,16 @@ export interface Direction { direction: Orientation; } -export type AppointmentEntity = ListEntity - & UTCDatesBeforeSplit +export type SortedEntity = ListEntity & AppointmentPart - & Level & Position + & Level + & AppointmentCollector + & SortedIndex; + +export type AppointmentEntity = SortedEntity & Direction & Empty - & SortedIndex & Geometry & AppointmentCollectorWithGeometry; diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts index 944f7e3b8616..55523a568f96 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_virtual_scrolling.ts @@ -691,11 +691,11 @@ export class VirtualScrollingRenderer { clearTimeout(this._renderAppointmentTimeoutID); this._renderAppointmentTimeoutID = setTimeout( - () => this.workspace.updateAppointments(), + () => this.workspace.renderAppointments(), renderTimeout, ); } else { - this.workspace.updateAppointments(); + this.workspace.renderAppointments(); } } } diff --git a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts index 6105e50b0732..e2756dab6488 100644 --- a/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts +++ b/packages/devextreme/js/__internal/scheduler/workspaces/m_work_space.ts @@ -2285,7 +2285,7 @@ class SchedulerWorkSpace extends Widget { draggingMode: 'outlook', onScrollEnd: () => {}, getHeaderHeight: undefined, - onRenderAppointments: () => {}, + renderAppointments: () => {}, onShowAllDayPanel: () => {}, onSelectedCellsClick: () => {}, timeZoneCalculator: undefined, @@ -2363,7 +2363,7 @@ class SchedulerWorkSpace extends Widget { break; case 'allDayPanelMode': this.updateShowAllDayPanel(); - this.updateAppointments(); + this.renderAppointments(); break; case 'width': // @ts-expect-error @@ -2888,8 +2888,8 @@ class SchedulerWorkSpace extends Widget { this.renderer._renderGrid(); } - updateAppointments() { - (this.option('onRenderAppointments') as any)(); + renderAppointments() { + (this.option('renderAppointments') as any)(); this.dragBehavior?.updateDragSource(); } From 7335366d2e054965f8b0fb3eb062fa6fbab87f08 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Thu, 5 Mar 2026 19:39:27 +0800 Subject: [PATCH 2/3] refactor fix --- .../appointments/m_appointment_collection.ts | 9 ++- .../appointments/m_appointments_kbn.ts | 73 +++++++++---------- .../js/__internal/scheduler/m_scheduler.ts | 4 +- .../view_model/appointments_layout_manager.ts | 1 + 4 files changed, 46 insertions(+), 41 deletions(-) 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 ae08c810549a..f09d5be00069 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -43,6 +43,7 @@ import type { AppointmentCollectorViewModel, AppointmentItemViewModel, AppointmentViewModelPlain, + SortedEntity, } from '../view_model/types'; import { AgendaAppointment } from './appointment/agenda_appointment'; import { Appointment } from './appointment/m_appointment'; @@ -106,6 +107,10 @@ class SchedulerAppointments extends CollectionWidget { return this.option('dataAccessors') as AppointmentDataAccessor; } + get sortedItems(): SortedEntity[] { + return this.option('sortedItems') as SortedEntity[]; + } + getResourceManager(): ResourceManager { return this.option('getResourceManager')(); } @@ -228,9 +233,9 @@ class SchedulerAppointments extends CollectionWidget { _optionChanged(args) { switch (args.name) { + case 'sortedItems': + break; case 'items': - console.log('render', this._kbn.focusedItemSortIndex); - this._cleanFocusState(); if (this.isAgendaView) { diff --git a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts index 0184bbf322f0..b489383636ad 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointments_kbn.ts @@ -5,12 +5,9 @@ import { getPublicElement } from '@ts/core/m_element'; import type { SupportedKeys } from '@ts/core/widget/widget'; import eventsEngine from '@ts/events/core/m_events_engine'; -import type { AppointmentViewModelPlain } from '../view_model/types'; +import type { SortedEntity } from '../view_model/types'; import type SchedulerAppointments from './m_appointment_collection'; -// Case when no appointments in Viewport -// Case when focusedAppointment is outside Viewport - export class AppointmentsKeyboardNavigation { private readonly _collection: SchedulerAppointments; @@ -38,6 +35,32 @@ export class AppointmentsKeyboardNavigation { } } + public $focusTarget(): dxElementWrapper { + const $items = this._collection.$itemBySortedIndex; + + if (!$items) { + return $(); + } + + if (this.focusedItemSortIndex !== -1) { + const $item = $items[this.focusedItemSortIndex]; + return $item || $(); + } + + const $itemsPlainArray = Object.values($items); + + const $firstItem = this._collection.isVirtualScrolling + ? $itemsPlainArray.find(($item) => this.isItemVisibleInViewport($item)) ?? $() + : $($itemsPlainArray[0]); + + return $firstItem; + } + + public resetTabIndex(): void { + this.getFocusableItems().attr('tabIndex', -1); + this.$focusTarget().attr('tabIndex', this._collection.option('tabIndex')); + } + public focusInHandler(e: DxEvent): void { const $target = $(e.target); const itemData = this._collection.getAppointmentSettings($target); @@ -70,31 +93,6 @@ export class AppointmentsKeyboardNavigation { }; } - public $focusTarget(): dxElementWrapper { - const $items = this._collection.$itemBySortedIndex; - - if (!$items?.length) { - return $(); - } - - if (this.focusedItemSortIndex === -1) { - const $itemsPlainArray = Object.values($items); - - return this._collection.isVirtualScrolling - ? $itemsPlainArray.find(($item) => this.isItemVisibleInViewport($item)) ?? $() - : $($itemsPlainArray[0]); - } - - const $item = $items[this.focusedItemSortIndex]; - - return $item || $(); - } - - public resetTabIndex(): void { - this.getFocusableItems().attr('tabIndex', -1); - this.$focusTarget().attr('tabIndex', this._collection.option('tabIndex')); - } - private delHandler(e: DxEvent): void { if (this._collection.option('allowDelete')) { e.preventDefault(); @@ -120,7 +118,7 @@ export class AppointmentsKeyboardNavigation { } private tabHandler(e: DxEvent): void { - const items = this._collection.option('getLayoutManager')().sortedItems; + const items = this._collection.sortedItems; const nextIndex = this.focusedItemSortIndex + (e.shiftKey ? -1 : 1); const nextItemData = items[nextIndex]; @@ -133,7 +131,7 @@ export class AppointmentsKeyboardNavigation { } private homeHandler(e: DxEvent): void { - const items = this._collection.option('getLayoutManager')().sortedItems; + const items = this._collection.sortedItems; const nextItemData = items[0]; if (!nextItemData) { @@ -145,7 +143,7 @@ export class AppointmentsKeyboardNavigation { } private endHandler(e: DxEvent): void { - const items = this._collection.option('getLayoutManager')().sortedItems; + const items = this._collection.sortedItems; const nextItemData = items[items.length - 1]; if (!nextItemData) { @@ -156,9 +154,10 @@ export class AppointmentsKeyboardNavigation { this.focusByItemData(nextItemData); } - private focusByItemData(itemData: AppointmentViewModelPlain): void { + private focusByItemData(itemData: SortedEntity): void { + this.focusedItemSortIndex = itemData.sortedIndex; + if (this._collection.isVirtualScrolling) { - this.focusedItemSortIndex = itemData.sortedIndex; this.isNavigating = true; this.scrollToByItemData(itemData); } @@ -166,10 +165,10 @@ export class AppointmentsKeyboardNavigation { this.focus(); } - private scrollToByItemData(itemData: AppointmentViewModelPlain): void { + private scrollToByItemData(itemData: SortedEntity): void { const date = new Date(Math.max( - this._collection.option('getStartViewDate')().getTime(), - (itemData as any).source.startDate, + this._collection.invoke('getStartViewDate').getTime(), + itemData.source.startDate, )); this._collection.option('scrollTo')( diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index 18ac5e06c02f..f162526e484d 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -872,8 +872,10 @@ class Scheduler extends SchedulerOptionsBaseWidget { ? this._layoutManager.generateViewModel() : []; + this._appointments.option('sortedItems', this._layoutManager.sortedItems); this._appointments.option('items', viewModel); this.appointmentDataSource.cleanState(); + if (this._isAgenda()) { this._workSpace.renderAgendaLayout(viewModel); } @@ -1234,9 +1236,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { _appointmentsConfig() { const config = { getResourceManager: () => this.resourceManager, - getLayoutManager: () => this._layoutManager, getAppointmentDataSource: () => this.appointmentDataSource, - getStartViewDate: this.getStartViewDate.bind(this), scrollTo: this.scrollTo.bind(this), dataAccessors: this._dataAccessors, notifyScheduler: this._notifyScheduler, diff --git a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts index 8ccc994ccf67..43939828151a 100644 --- a/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts +++ b/packages/devextreme/js/__internal/scheduler/view_model/appointments_layout_manager.ts @@ -113,6 +113,7 @@ class AppointmentLayoutManager { height: item.height, info: getAppointmentInfo(item), } as unknown as AppointmentItemViewModel); + return viewModel.map((item) => { if (item.items.length) { return { From e921c5235459530f8fe24ce6d3c15f7435fb0502 Mon Sep 17 00:00:00 2001 From: Eldar Iusupzhanov Date: Thu, 5 Mar 2026 20:54:34 +0800 Subject: [PATCH 3/3] minor fix refactoring --- .../appointments/m_appointment_collection.ts | 14 +++++++------- .../js/__internal/scheduler/m_scheduler.ts | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) 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 f09d5be00069..5fba5e7e9fee 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/m_appointment_collection.ts @@ -108,7 +108,7 @@ class SchedulerAppointments extends CollectionWidget { } get sortedItems(): SortedEntity[] { - return this.option('sortedItems') as SortedEntity[]; + return this.option('getSortedAppointments')() as SortedEntity[]; } getResourceManager(): ResourceManager { @@ -146,7 +146,8 @@ class SchedulerAppointments extends CollectionWidget { const kbnValue = this._kbn.getSupportedKeys(); return { - ...parentValue, + enter: parentValue.enter, + space: parentValue.space, ...kbnValue, }; } @@ -233,8 +234,6 @@ class SchedulerAppointments extends CollectionWidget { _optionChanged(args) { switch (args.name) { - case 'sortedItems': - break; case 'items': this._cleanFocusState(); @@ -299,9 +298,11 @@ class SchedulerAppointments extends CollectionWidget { protected repaintAppointments(diff: ViewModelDiff[]): void { this.$itemBySortedIndex = []; + this._renderByFragments(($commonFragment, $allDayFragment) => { - const isRepaintAll = this.isAgendaView - || !diff.some((item) => item.needToAdd === undefined && item.needToRemove === undefined); + const isRepaintAll = diff.every( + (item) => Boolean(item.needToAdd ?? item.needToRemove), + ); if (isRepaintAll) { this._getAppointmentContainer(true).html(''); @@ -508,7 +509,6 @@ class SchedulerAppointments extends CollectionWidget { } _render() { - (window as any).y = this; super._render(); this._attachAppointmentDblClick(); } diff --git a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts index f162526e484d..5f78323305ee 100644 --- a/packages/devextreme/js/__internal/scheduler/m_scheduler.ts +++ b/packages/devextreme/js/__internal/scheduler/m_scheduler.ts @@ -872,7 +872,6 @@ class Scheduler extends SchedulerOptionsBaseWidget { ? this._layoutManager.generateViewModel() : []; - this._appointments.option('sortedItems', this._layoutManager.sortedItems); this._appointments.option('items', viewModel); this.appointmentDataSource.cleanState(); @@ -1237,6 +1236,7 @@ class Scheduler extends SchedulerOptionsBaseWidget { const config = { getResourceManager: () => this.resourceManager, getAppointmentDataSource: () => this.appointmentDataSource, + getSortedAppointments: () => this._layoutManager.sortedItems, scrollTo: this.scrollTo.bind(this), dataAccessors: this._dataAccessors, notifyScheduler: this._notifyScheduler,