Skip to content
Open
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
19 changes: 19 additions & 0 deletions packages/devextreme/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,25 @@ export default [
'devextreme-custom/no-deferred': 'off',
},
},
// Strict TypeScript rules for scheduler/header
{
files: ['js/__internal/scheduler/header/**/*.ts?(x)'],
languageOptions: {
parser: tsParser,
ecmaVersion: 5,
sourceType: 'script',
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: `${__dirname}/js/__internal`,
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'tsconfigRootDir' path appears to be incorrect. It's set to '${__dirname}/js/__internal', but the tsconfig.json is located at the package root. This should likely be just '__dirname' or the path should correctly resolve to where tsconfig.json exists. This misconfiguration could cause TypeScript parsing issues in the ESLint rules.

Copilot uses AI. Check for mistakes.
},
},
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'error',
'@typescript-eslint/no-unsafe-return': 'error',
'@typescript-eslint/explicit-module-boundary-types': 'error',
},
},
// Rules for grid controls
{
files: [
Expand Down
95 changes: 54 additions & 41 deletions packages/devextreme/js/__internal/scheduler/header/m_calendar.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,66 @@
import registerComponent from '@js/core/component_registrator';
import devices from '@js/core/devices';
import type { dxElementWrapper } from '@js/core/renderer';
import $ from '@js/core/renderer';
import Calendar from '@js/ui/calendar';
import type { DateLike } from '@js/ui/calendar';
import Popover from '@js/ui/popover/ui.popover';
import Popup from '@js/ui/popup/ui.popup';
import type { dxSchedulerOptions } from '@js/ui/scheduler';
import Scrollable from '@js/ui/scroll_view/ui.scrollable';
import Widget from '@js/ui/widget/ui.widget';
import Widget from '@ts/core/widget/widget';
import type { KeyboardKeyDownEvent } from '@ts/events/core/m_keyboard_processor';
import type { CalendarProperties } from '@ts/ui/calendar/calendar';
import Calendar from '@ts/ui/calendar/calendar';
import Scrollable from '@ts/ui/scroll_view/scrollable';

import type { HeaderCalendarOptions } from './types';

const CALENDAR_CLASS = 'dx-scheduler-navigator-calendar';
const CALENDAR_POPOVER_CLASS = 'dx-scheduler-navigator-calendar-popover';

export default class SchedulerCalendar extends Widget<dxSchedulerOptions> {
_overlay: any;
export default class SchedulerCalendar extends Widget<HeaderCalendarOptions> {
_overlay?: Popup | Popover;

_calendar: any;
_calendar?: Calendar;

show(target) {
if (!this._isMobileLayout()) {
this._overlay.option('target', target);
async show(target: HTMLElement): Promise<void> {
if (!SchedulerCalendar._isMobileLayout()) {
this._overlay?.option('target', target);
}
this._overlay.show();

await this._overlay?.show();
}

hide() {
this._overlay.hide();
async hide(): Promise<void> {
await this._overlay?.hide();
}

_keyboardHandler(opts): void {
_keyboardHandler(opts: KeyboardKeyDownEvent): boolean {
this._calendar?._keyboardHandler(opts);
return true;
Comment on lines 31 to +38
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method always returns true regardless of whether _calendar exists or what _calendar._keyboardHandler returns. This could mask keyboard handling failures. Consider returning false when _calendar is undefined, or returning the result from _calendar._keyboardHandler(opts) if it returns a boolean.

Suggested change
this._calendar?._keyboardHandler(opts);
return true;
const handled = this._calendar?._keyboardHandler(opts);
if (typeof handled === 'boolean') {
return handled;
}
return !!this._calendar;

Copilot uses AI. Check for mistakes.
}

_init(): void {
// @ts-expect-error
super._init();
this.$element();
}

_render(): void {
// @ts-expect-error
super._render();
this._renderOverlay();
}

_renderOverlay(): void {
this.$element().addClass(CALENDAR_POPOVER_CLASS);

const isMobileLayout = this._isMobileLayout();
const isMobileLayout = SchedulerCalendar._isMobileLayout();

const overlayType = isMobileLayout ? Popup : Popover;

// @ts-expect-error
this._overlay = this._createComponent(this.$element(), overlayType, {
contentTemplate: () => this._createOverlayContent(),
onShown: () => this._calendar.focus(),
const overlayConfig = {
contentTemplate: (): dxElementWrapper => this._createOverlayContent(),
onShown: (): void => {
this._calendar?.focus();
},
defaultOptionsRules: [
{
device: () => isMobileLayout,
device: (): boolean => isMobileLayout,
options: {
fullScreen: true,
showCloseButton: false,
Expand All @@ -67,24 +71,28 @@ export default class SchedulerCalendar extends Widget<dxSchedulerOptions> {
},
},
],
});
};

if (isMobileLayout) {
this._overlay = this._createComponent(this.$element(), Popup, overlayConfig);
} else {
this._overlay = this._createComponent(this.$element(), Popover, overlayConfig);
}
}

_createOverlayContent() {
_createOverlayContent(): dxElementWrapper {
const result = $('<div>').addClass(CALENDAR_CLASS);
// @ts-expect-error
this._calendar = this._createComponent(result, Calendar, this._getCalendarOptions());

if (this._isMobileLayout()) {
if (SchedulerCalendar._isMobileLayout()) {
const scrollable = this._createScrollable(result);
return scrollable.$element();
}

return result;
}

_createScrollable(content) {
// @ts-expect-error
_createScrollable(content: dxElementWrapper): Scrollable {
const result = this._createComponent('<div>', Scrollable, {
height: 'auto',
direction: 'both',
Expand All @@ -94,7 +102,9 @@ export default class SchedulerCalendar extends Widget<dxSchedulerOptions> {
return result;
}

_optionChanged({ name, value }) {
_optionChanged(
{ name, value } : { name: string; value: DateLike | DateLike[] },
): void {
switch (name) {
case 'value':
this._calendar?.option('value', value);
Expand All @@ -104,23 +114,26 @@ export default class SchedulerCalendar extends Widget<dxSchedulerOptions> {
}
}

_getCalendarOptions() {
_getCalendarOptions(): CalendarProperties {
const {
value, min, max, firstDayOfWeek, focusStateEnabled, tabIndex, onValueChanged,
} = this.option();
return {
value: this.option('value'),
min: this.option('min'),
max: this.option('max'),
firstDayOfWeek: this.option('firstDayOfWeek'),
focusStateEnabled: this.option('focusStateEnabled'),
onValueChanged: this.option('onValueChanged'),
value,
min,
max,
firstDayOfWeek,
focusStateEnabled,
tabIndex,
onValueChanged,
// @ts-expect-error skipFocusCheck is an internal Calendar property
skipFocusCheck: true,
tabIndex: this.option('tabIndex'),
};
}

_isMobileLayout() {
static _isMobileLayout(): boolean {
return !devices.current().generic;
}
}

// @ts-expect-error
registerComponent('dxSchedulerCalendarPopup', SchedulerCalendar);
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {
describe, expect, it, jest,
} from '@jest/globals';
import type { ToolbarItem } from '@js/ui/scheduler';

import {
CLASS, DEFAULT_ITEMS, getDateNavigator, ITEMS_NAME,
} from './m_date_navigator';
import type { SchedulerHeader } from './m_header';

describe('getDateNavigator', () => {
it('should return default options in case of item is empty', () => {
expect(getDateNavigator({} as any, {})).toEqual({
expect(getDateNavigator({} as SchedulerHeader, {})).toEqual({
location: 'before',
name: 'dateNavigator',
widget: 'dxButtonGroup',
Expand All @@ -26,13 +28,13 @@ describe('getDateNavigator', () => {
});
});
it('should return replace items in correct order with custom options', () => {
expect(getDateNavigator({} as any, {
expect(getDateNavigator({} as SchedulerHeader, {
customField: 'customField',
options: {
customOption: 'customOption',
items: ['dateInterval', 'next', { key: 'customButton' }],
},
} as any)).toEqual({
} as ToolbarItem)).toEqual({
location: 'before',
name: 'dateNavigator',
widget: 'dxButtonGroup',
Expand All @@ -54,7 +56,7 @@ describe('getDateNavigator', () => {
it('should handle default and custom click callback', () => {
const customClick = jest.fn();
const event = { itemData: { clickHandler: jest.fn() } };
const config = getDateNavigator({} as any, {
const config = getDateNavigator({} as SchedulerHeader, {
options: { onItemClick: customClick },
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import messageLocalization from '@js/common/core/localization/message';
import dateUtils from '@js/core/utils/date';
import type { ContentReadyEvent } from '@js/ui/button';
import type { Item as ButtonGroupItem, ItemClickEvent, Properties as ButtonGroupOptions } from '@js/ui/button_group';
import { isMaterialBased } from '@js/ui/themes';
import { current, isMaterialBased } from '@js/ui/themes';
import type { Item as ToolbarItem } from '@js/ui/toolbar';
import { dateUtilsTs } from '@ts/core/utils/date';
import { extend } from '@ts/core/utils/m_extend';
Expand Down Expand Up @@ -31,7 +31,7 @@ const { trimTime } = dateUtils;

interface DateNavigatorItem extends ButtonGroupItem {
key: string;
clickHandler: (event: ItemClickEvent) => void;
clickHandler: (event: ItemClickEvent) => Promise<void> | void;
onContentReady: (event: ContentReadyEvent) => void;
}

Expand Down Expand Up @@ -154,9 +154,28 @@ const getNextButtonOptions = (header: SchedulerHeader): DateNavigatorItem => {
};
};

export const getTodayButtonOptions = (
header: SchedulerHeader,
item: ToolbarItem,
): ToolbarItem => extend(true, {}, {
location: 'before',
locateInMenu: 'auto',
widget: 'dxButton',
cssClass: 'dx-scheduler-today',
options: {
text: messageLocalization.format('dxScheduler-navigationToday'),
icon: 'today',
stylingMode: 'outlined',
type: 'normal',
onClick() {
const { indicatorTime } = header.option();
header._updateCurrentDate(indicatorTime ?? new Date());
},
},
}, item) as ToolbarItem;

export const getDateNavigator = (header: SchedulerHeader, item: ToolbarItem): ToolbarItem => {
// @ts-expect-error current theme used
const stylingMode = isMaterialBased() ? 'text' : 'contained';
const stylingMode = isMaterialBased(current()) ? 'text' : 'contained';
const config: ToolbarItem = extend(true, {}, {
location: 'before',
name: 'dateNavigator',
Expand All @@ -170,7 +189,8 @@ export const getDateNavigator = (header: SchedulerHeader, item: ToolbarItem): To
const options = config.options as ButtonGroupOptions;
const { onItemClick } = options;

options.items = (options.items ?? DEFAULT_ITEMS).map((groupItem) => {
const items = (options.items ?? DEFAULT_ITEMS);
options.items = items.map((groupItem: ButtonGroupItem | string) => {
switch (groupItem) {
case ITEMS_NAME.previousButton:
return getPreviousButtonOptions(header);
Expand All @@ -179,7 +199,7 @@ export const getDateNavigator = (header: SchedulerHeader, item: ToolbarItem): To
case ITEMS_NAME.calendarButton:
return getCalendarButtonOptions(header);
default:
return groupItem;
return groupItem as ButtonGroupItem;
}
});
options.onItemClick = (event): void => {
Expand Down
Loading
Loading