From 7ce45176bb0a253cf17003d8d305a3501cca5d17 Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Mon, 4 May 2026 17:14:03 +0300 Subject: [PATCH 1/2] RadioGroup: Improve types --- .../ui/radio_group/m_radio_button.ts | 22 ++-- .../ui/radio_group/m_radio_collection.ts | 34 +++--- .../ui/radio_group/m_radio_group.ts | 115 +++++++++--------- 3 files changed, 89 insertions(+), 82 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/radio_group/m_radio_button.ts b/packages/devextreme/js/__internal/ui/radio_group/m_radio_button.ts index 9de60a290d3a..bc57525482a4 100644 --- a/packages/devextreme/js/__internal/ui/radio_group/m_radio_button.ts +++ b/packages/devextreme/js/__internal/ui/radio_group/m_radio_button.ts @@ -8,7 +8,7 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { DxEvent } from '@js/events'; import type { OptionChanged } from '@ts/core/widget/types'; -import type { EditorProperties } from '@ts/ui/editor/editor'; +import type { EditorProperties, ValueChangedEvent } from '@ts/ui/editor/editor'; import Editor from '@ts/ui/editor/editor'; const RADIO_BUTTON_CLASS = 'dx-radiobutton'; @@ -24,11 +24,10 @@ class RadioButton extends Editor { _clickAction?: (event?: Record) => void; - // eslint-disable-next-line @typescript-eslint/no-invalid-void-type - _supportedKeys(): Record void | boolean> { - const click = function (e) { + _supportedKeys(): Record void> { + const click = (e: KeyboardEvent): void => { e.preventDefault(); - this._clickAction({ event: e }); + this._clickAction?.({ event: e }); }; return { ...super._supportedKeys(), @@ -70,10 +69,12 @@ class RadioButton extends Editor { } _initMarkup(): void { + const { value } = this.option(); + super._initMarkup(); this._renderIcon(); - this._renderCheckedState(this.option('value')); + this._renderCheckedState(value); this._renderClick(); this.setAria('role', 'radio'); } @@ -85,7 +86,7 @@ class RadioButton extends Editor { this.$element().append(this._$icon); } - _renderCheckedState(checked): void { + _renderCheckedState(checked: boolean): void { this.$element() .toggleClass(RADIO_BUTTON_CHECKED_CLASS, checked) .find(`.${RADIO_BUTTON_ICON_CLASS}`) @@ -94,10 +95,9 @@ class RadioButton extends Editor { } _renderClick(): void { - // @ts-expect-error ts-error - const eventName = addNamespace(clickEventName, this.NAME); + const eventName = addNamespace(clickEventName, this.NAME ?? ''); - this._clickAction = this._createAction((args): void => { + this._clickAction = this._createAction((args: { event: ValueChangedEvent }): void => { this._clickHandler(args.event); }); @@ -107,7 +107,7 @@ class RadioButton extends Editor { }); } - _clickHandler(e): void { + _clickHandler(e: ValueChangedEvent): void { this._saveValueChangeEvent(e); this.option('value', true); this._saveValueChangeEvent(undefined); diff --git a/packages/devextreme/js/__internal/ui/radio_group/m_radio_collection.ts b/packages/devextreme/js/__internal/ui/radio_group/m_radio_collection.ts index 2affce54281f..4fa5628bc8fe 100644 --- a/packages/devextreme/js/__internal/ui/radio_group/m_radio_collection.ts +++ b/packages/devextreme/js/__internal/ui/radio_group/m_radio_collection.ts @@ -2,9 +2,10 @@ import Guid from '@js/core/guid'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { deferRender } from '@js/core/utils/common'; -import { extend } from '@js/core/utils/extend'; +import type { DxEvent } from '@js/events'; +import type { CollectionWidgetItem as CollectionWidgetItemProperties } from '@js/ui/collection/ui.collection_widget.base'; import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; -import type { CollectionWidgetBaseProperties } from '@ts/ui/collection/collection_widget.base'; +import type { CollectionWidgetBaseProperties, PostprocessRenderItemInfo } from '@ts/ui/collection/collection_widget.base'; import CollectionWidget from '@ts/ui/collection/collection_widget.edit'; const RADIO_BUTTON_CHECKED_CLASS = 'dx-radiobutton-checked'; @@ -29,10 +30,12 @@ class RadioCollection extends CollectionWidget { } _getDefaultOptions(): Properties { - const defaultOptions = super._getDefaultOptions(); - - // @ts-expect-error - return extend(defaultOptions, DataExpressionMixin._dataExpressionDefaultOptions()); + return { + ...super._getDefaultOptions(), + // @ts-expect-error DataExpressionMixin._dataExpressionDefaultOptions() + // should be added to the type + ...DataExpressionMixin._dataExpressionDefaultOptions(), + } as Properties; } _initMarkup(): void { @@ -59,7 +62,7 @@ class RadioCollection extends CollectionWidget { return $target; } - _postprocessRenderItem(args): void { + _postprocessRenderItem(args: PostprocessRenderItemInfo): void { const { itemData, itemElement } = args; const { html } = itemData; @@ -126,22 +129,21 @@ class RadioCollection extends CollectionWidget { this._renderContent(); } - _supportedKeys(): Record void> { + _supportedKeys(): Record) => void> { const parent = super._supportedKeys(); - return extend({}, parent, { - enter(e) { + return { + ...parent, + enter(e: DxEvent): void { e.preventDefault(); - // @ts-expect-error - return parent.enter.apply(this, arguments); + parent.enter?.apply(this, [e]); }, - space(e) { + space(e: DxEvent): void { e.preventDefault(); - // @ts-expect-error - return parent.space.apply(this, arguments); + parent.space?.apply(this, [e]); }, - }); + }; } _itemElements(): dxElementWrapper { diff --git a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts index 6a9c0c7a09af..2399a846ef51 100644 --- a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts +++ b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts @@ -5,12 +5,12 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; -import { extend } from '@js/core/utils/extend'; import { isDefined } from '@js/core/utils/type'; +import type { CollectionWidgetItem } from '@js/ui/collection/ui.collection_widget.base'; import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; import type { Properties } from '@js/ui/radio_group'; import type { OptionChanged } from '@ts/core/widget/types'; -import type { EditorProperties, UnresolvedEvents } from '@ts/ui/editor/editor'; +import type { EditorProperties, UnresolvedEvents, ValueChangedEvent } from '@ts/ui/editor/editor'; import Editor from '@ts/ui/editor/editor'; import RadioCollection from './m_radio_collection'; @@ -28,21 +28,18 @@ interface RadioGroupProperties extends Properties, class RadioGroup extends Editor { private _radios?: RadioCollection; - private _areRadiosCreated!: DeferredObj; + private _areRadiosCreated!: DeferredObj; private _$submitElement!: dxElementWrapper; - // eslint-disable-next-line class-methods-use-this _dataSourceOptions(): { paginate: boolean } { return { paginate: false }; } - // eslint-disable-next-line class-methods-use-this protected _activeStateUnit(): string { return `.${RADIO_BUTTON_CLASS}`; } - // eslint-disable-next-line class-methods-use-this protected _feedbackHideTimeout(): number { return RADIO_FEEDBACK_HIDE_TIMEOUT; } @@ -63,32 +60,38 @@ class RadioGroup extends Editor { }]); } - // @ts-expect-error + // @ts-expect-error widget.ts _fireContentReadyAction should accept an optional `force` parameter _fireContentReadyAction(force: boolean): void { - force && super._fireContentReadyAction(); + if (!force) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-floating-promises + super._fireContentReadyAction(); } - _focusTarget() { + _focusTarget(): dxElementWrapper { return this.$element(); } - _getAriaTarget() { + _getAriaTarget(): dxElementWrapper { return this.$element(); } - _getDefaultOptions() { - const defaultOptions = super._getDefaultOptions(); - - // @ts-expect-error - return extend(defaultOptions, extend(DataExpressionMixin._dataExpressionDefaultOptions(), { + _getDefaultOptions(): RadioGroupProperties { + return { + ...super._getDefaultOptions(), + // @ts-expect-error DataExpressionMixin should expose _dataExpressionDefaultOptions as a + // typed static method + ...DataExpressionMixin._dataExpressionDefaultOptions(), hoverStateEnabled: true, activeStateEnabled: true, layout: 'vertical', - })); + } as RadioGroupProperties; } - _getItemValue(item) { - // @ts-expect-error + _getItemValue(item: CollectionWidgetItem): unknown { + // @ts-expect-error valueGetter is injected by DataExpressionMixin return this._valueGetter ? this._valueGetter(item) : item.text; } @@ -99,7 +102,7 @@ class RadioGroup extends Editor { _init(): void { super._init(); - // @ts-expect-error + // @ts-expect-error _initDataExpressions is injected by DataExpressionMixin this._initDataExpressions(); } @@ -112,28 +115,32 @@ class RadioGroup extends Editor { super._initMarkup(); } - _itemClickHandler({ itemElement, event, itemData }): void { - // @ts-expect-error - if (this.itemElements().is(itemElement)) { + _itemClickHandler({ itemElement, event, itemData }: { + itemElement: Element; + event: ValueChangedEvent; + itemData: CollectionWidgetItem; + }): void { + if (this.itemElements()?.is($(itemElement))) { + const { value } = this.option(); const newValue = this._getItemValue(itemData); - if (newValue !== this.option('value')) { + if (newValue !== value) { this._saveValueChangeEvent(event); this.option('value', newValue); } } } - _getSelectedItemKeys(value) { - // @ts-expect-error - const isNullSelectable = this.option('valueExpr') !== 'this'; - const shouldSelectValue = isNullSelectable && value === null || isDefined(value); + _getSelectedItemKeys(value: unknown): unknown[] { + const { valueExpr } = this.option(); + const isNullSelectable = valueExpr !== 'this'; + const shouldSelectValue = (isNullSelectable && value === null) || isDefined(value); return shouldSelectValue ? [value] : []; } - _setSelection(currentValue): void { - // @ts-expect-error + _setSelection(currentValue: unknown): void { + // @ts-expect-error _unwrappedValue is injected by DataExpressionMixin const value = this._unwrappedValue(currentValue); this._setCollectionWidgetOption('selectedItemKeys', this._getSelectedItemKeys(value)); } @@ -152,7 +159,7 @@ class RadioGroup extends Editor { _optionChanged(args: OptionChanged): void { const { name, value } = args; - // @ts-expect-error + // @ts-expect-error _dataExpressionOptionChanged is injected by DataExpressionMixin this._dataExpressionOptionChanged(args); switch (name) { @@ -169,7 +176,7 @@ class RadioGroup extends Editor { this._setCollectionWidgetOption(name, value); break; case 'valueExpr': - // @ts-expect-error + // @ts-expect-error _getCollectionKeyExpr is injected by DataExpressionMixin this._setCollectionWidgetOption('keyExpr', this._getCollectionKeyExpr()); break; case 'value': @@ -178,7 +185,7 @@ class RadioGroup extends Editor { super._optionChanged(args); break; case 'items': - this._setSelection(this.option('value')); + this._setSelection(this.option().value); break; case 'itemTemplate': case 'displayExpr': @@ -220,19 +227,19 @@ class RadioGroup extends Editor { onInitialized: ({ component }) => { this._radios = component; }, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onContentReady: (e) => { + onContentReady: () => { this._fireContentReadyAction(true); }, - // @ts-expect-error + // @ts-expect-error RadioCollection.Properties should define onItemClick with the + // correct internal callback type onItemClick: this._itemClickHandler.bind(this), displayExpr, accessKey, - // @ts-expect-error + // @ts-expect-error _dataSource is injected by DataExpressionMixin dataSource: this._dataSource, focusStateEnabled, itemTemplate, - // @ts-expect-error + // @ts-expect-error _getCollectionKeyExpr is injected by DataExpressionMixin keyExpr: this._getCollectionKeyExpr(), noDataText: '', scrollingEnabled: false, @@ -254,35 +261,33 @@ class RadioGroup extends Editor { _setOptionsByReference(): void { super._setOptionsByReference(); - extend(this._optionsByReference, { value: true }); + this._optionsByReference = { ...this._optionsByReference, value: true }; } _setSubmitValue(value?: unknown): void { - value = value ?? this.option('value'); - // @ts-expect-error - const submitValue = this.option('valueExpr') === 'this' - // @ts-expect-error - ? this._displayGetter(value) - : value; - - this._$submitElement.val(submitValue); - } + const { valueExpr, value: optionValue } = this.option(); + const resolvedValue = value ?? optionValue; + + const submitValue = valueExpr === 'this' + // @ts-expect-error _displayGetter is injected by DataExpressionMixin + ? this._displayGetter(resolvedValue) + : resolvedValue; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _setCollectionWidgetOption(name: string, value: unknown): void { - // @ts-expect-error + this._$submitElement.val(submitValue as string | number | undefined); + } - this._areRadiosCreated.done(this._setWidgetOption.bind(this, '_radios', arguments)); + _setCollectionWidgetOption(...args: [string, unknown]): void { + // @ts-expect-error widget._setWidgetOption args should be typed as ArrayLike + this._areRadiosCreated.done(this._setWidgetOption.bind(this, '_radios', args)); } _updateItemsSize(): void { - const { layout } = this.option(); + const { layout, items } = this.option(); if (layout === 'horizontal') { this.itemElements()?.css('height', 'auto'); } else { - // @ts-expect-error - const itemsCount = this.option('items').length; + const itemsCount = (items ?? []).length; this.itemElements()?.css('height', `${100 / itemsCount}%`); } @@ -296,7 +301,7 @@ class RadioGroup extends Editor { return this._radios?._itemElements(); } } -// @ts-expect-error +// @ts-expect-error Widget base class should define a typed static include() method RadioGroup.include(DataExpressionMixin); registerComponent('dxRadioGroup', RadioGroup); From 308d3b3433537cee1210dba81f5ddb4db443150c Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Mon, 4 May 2026 17:56:28 +0300 Subject: [PATCH 2/2] RadioGroup: Replace CollectionWidgetItem with ItemLike Co-authored-by: Copilot --- .../__internal/ui/radio_group/m_radio_group.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts index 2399a846ef51..73d21c825de7 100644 --- a/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts +++ b/packages/devextreme/js/__internal/ui/radio_group/m_radio_group.ts @@ -6,7 +6,8 @@ import $ from '@js/core/renderer'; import type { DeferredObj } from '@js/core/utils/deferred'; import { Deferred } from '@js/core/utils/deferred'; import { isDefined } from '@js/core/utils/type'; -import type { CollectionWidgetItem } from '@js/ui/collection/ui.collection_widget.base'; +import type { ItemInfo, NativeEventInfo } from '@js/events'; +import type { ItemLike } from '@js/ui/collection/ui.collection_widget.base'; import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; import type { Properties } from '@js/ui/radio_group'; import type { OptionChanged } from '@ts/core/widget/types'; @@ -90,7 +91,7 @@ class RadioGroup extends Editor { } as RadioGroupProperties; } - _getItemValue(item: CollectionWidgetItem): unknown { + _getItemValue(item: ItemLike): unknown { // @ts-expect-error valueGetter is injected by DataExpressionMixin return this._valueGetter ? this._valueGetter(item) : item.text; } @@ -115,17 +116,14 @@ class RadioGroup extends Editor { super._initMarkup(); } - _itemClickHandler({ itemElement, event, itemData }: { - itemElement: Element; - event: ValueChangedEvent; - itemData: CollectionWidgetItem; - }): void { + _itemClickHandler({ itemElement, event, itemData }: + NativeEventInfo & ItemInfo): void { if (this.itemElements()?.is($(itemElement))) { const { value } = this.option(); const newValue = this._getItemValue(itemData); if (newValue !== value) { - this._saveValueChangeEvent(event); + this._saveValueChangeEvent(event as unknown as ValueChangedEvent); this.option('value', newValue); } } @@ -230,8 +228,6 @@ class RadioGroup extends Editor { onContentReady: () => { this._fireContentReadyAction(true); }, - // @ts-expect-error RadioCollection.Properties should define onItemClick with the - // correct internal callback type onItemClick: this._itemClickHandler.bind(this), displayExpr, accessKey, @@ -242,6 +238,7 @@ class RadioGroup extends Editor { // @ts-expect-error _getCollectionKeyExpr is injected by DataExpressionMixin keyExpr: this._getCollectionKeyExpr(), noDataText: '', + // @ts-expect-error scrollingEnabled is absent from CollectionWidgetProperties scrollingEnabled: false, selectByClick: false, selectionMode: 'single',