Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
df229e2
refactor: add types to m_calendar
sjbur Jan 21, 2026
bcc5570
refactor: add types to m_header
sjbur Jan 21, 2026
ec8a94a
feat: add types to m_date_navigator
sjbur Jan 21, 2026
9ec0d4e
refactor: add types to m_utils in header
sjbur Jan 21, 2026
56a217c
refactor: move today button to m_date_navigator
sjbur Jan 21, 2026
58d7324
refactor: revert changes to m_date of js/core
sjbur Jan 21, 2026
ce2e03a
eslint: add strict ts rules to header folder
sjbur Jan 21, 2026
2ea8c28
refactor: introduce new folder structure for header
sjbur Jan 21, 2026
1747a00
Revert "refactor: introduce new folder structure for header"
sjbur Jan 21, 2026
bb9648f
fix: fix bug
sjbur Jan 21, 2026
2e72021
fix: fix ts lint
sjbur Jan 21, 2026
a312599
refactor: small updates to syntax
sjbur Jan 22, 2026
0dc2548
refactor: small updates to syntax
sjbur Jan 22, 2026
ebf6cec
refactor: small updates to syntax
sjbur Jan 22, 2026
097b90a
refactor: rename scheduler calendar header type
sjbur Jan 22, 2026
dd4dc57
fix: fix build
sjbur Jan 22, 2026
7e10d94
refactor: small updates to syntax
sjbur Jan 22, 2026
c5434c1
Apply suggestion from @Copilot
sjbur Jan 22, 2026
be2f023
Merge branch '26_1' into issue-1874_26_1
sjbur Jan 22, 2026
b4ab34f
Merge branch '26_1' into issue-1874_26_1
sjbur Jan 23, 2026
9acc2bd
feat: add public/private modificators
sjbur Jan 23, 2026
4afb151
refactor(scheduler): simplify getPeriodEndDate logic
sjbur Jan 23, 2026
5ecd11c
Merge branch '26_1' into issue-1874_26_1
sjbur Jan 26, 2026
afc57ad
feat: apply changes
sjbur Jan 26, 2026
1407747
Merge remote-tracking branch 'origin/issue-1874_26_1' into issue-1874…
sjbur Jan 26, 2026
3119625
feat: apply changes
sjbur Jan 26, 2026
9b3c66c
fix: return _getDefaultOptions
sjbur Jan 26, 2026
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
104 changes: 59 additions & 45 deletions packages/devextreme/js/__internal/scheduler/header/m_calendar.ts
Original file line number Diff line number Diff line change
@@ -1,62 +1,65 @@
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 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 type { OptionChanged } from '@ts/core/widget/types';
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);
public 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();
public async hide(): Promise<void> {
await this._overlay?.hide();
}

_keyboardHandler(opts): void {
this._calendar?._keyboardHandler(opts);
public _keyboardHandler(opts: KeyboardKeyDownEvent): boolean {
return this._calendar?._keyboardHandler(opts) ?? false;
}

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

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

_renderOverlay(): void {
private _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 +70,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() {
private _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
private _createScrollable(content: dxElementWrapper): Scrollable {
const result = this._createComponent('<div>', Scrollable, {
height: 'auto',
direction: 'both',
Expand All @@ -94,7 +101,11 @@ export default class SchedulerCalendar extends Widget<dxSchedulerOptions> {
return result;
}

_optionChanged({ name, value }) {
public _optionChanged(
args: OptionChanged<HeaderCalendarOptions>,
): void {
const { name, value } = args;

switch (name) {
case 'value':
this._calendar?.option('value', value);
Expand All @@ -104,23 +115,26 @@ export default class SchedulerCalendar extends Widget<dxSchedulerOptions> {
}
}

_getCalendarOptions() {
private _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() {
private 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,12 +31,12 @@ const { trimTime } = dateUtils;

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

const isPreviousButtonDisabled = (header: SchedulerHeader): boolean => {
const minOption = header.option('min');
const minOption = header.option().min;

if (!dateUtilsTs.isValidDate(minOption)) return false;

Expand All @@ -50,7 +50,7 @@ const isPreviousButtonDisabled = (header: SchedulerHeader): boolean => {
};

const isNextButtonDisabled = (header: SchedulerHeader): boolean => {
const maxOption = header.option('max');
const maxOption = header.option().max;

if (!dateUtilsTs.isValidDate(maxOption)) return false;

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