From 5f55813e4fc2af492fa72b0932629af837a8201d Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Thu, 28 May 2026 17:38:52 +0300 Subject: [PATCH 1/4] DropDownEditor: Improve types --- .../ui/date_box/m_date_box.strategy.list.ts | 3 +- .../ui/drop_down_editor/m_drop_down_editor.ts | 237 ++++++++++-------- .../__internal/ui/drop_down_editor/m_utils.ts | 21 +- .../devextreme/js/__internal/ui/m_lookup.ts | 9 +- 4 files changed, 150 insertions(+), 120 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts index a3d0f327d88e..f71134cb01c1 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.list.ts @@ -324,7 +324,8 @@ class ListStrategy extends DateBoxStrategy { } _updatePopupHeight(): void { - const dropDownOptionsHeight = getSizeValue(this.dateBox.option('dropDownOptions.height')); + const { dropDownOptions } = this.dateBox.option(); + const dropDownOptionsHeight = getSizeValue(dropDownOptions?.height); if (dropDownOptionsHeight === undefined || dropDownOptionsHeight === 'auto') { this.dateBox._setPopupOption('height', 'auto'); diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts index 7f83c9b7abd5..1d6c6a772724 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts @@ -1,3 +1,4 @@ +import type { Mode, Position } from '@js/common'; import type { PositionConfig } from '@js/common/core/animation'; import animationPosition from '@js/common/core/animation/position'; import { locate, move } from '@js/common/core/animation/translator'; @@ -15,12 +16,11 @@ import $ from '@js/core/renderer'; import { FunctionTemplate } from '@js/core/templates/function_template'; import browser from '@js/core/utils/browser'; import { - // @ts-expect-error + // @ts-expect-error fix on core/utils level splitPair, } from '@js/core/utils/common'; import type { DeferredObj } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; import { getDefaultAlignment } from '@js/core/utils/position'; import { isDefined } from '@js/core/utils/type'; import { hasWindow } from '@js/core/utils/window'; @@ -30,12 +30,13 @@ import type { PointerInteractionEvent, } from '@js/events'; import type { Properties } from '@js/ui/drop_down_editor/ui.drop_down_editor'; -import type { Properties as PopupProperties } from '@js/ui/popup'; +import type { InitializedEvent as PopupInitializedEvent, Properties as PopupProperties, ToolbarItem } from '@js/ui/popup'; import Popup from '@js/ui/popup/ui.popup'; import errors from '@js/ui/widget/ui.errors'; import Widget from '@js/ui/widget/ui.widget'; import { focused } from '@ts/core/utils/m_selectors'; import type { OptionChanged } from '@ts/core/widget/types'; +import type { PositioningEvent } from '@ts/ui/overlay/overlay'; import TextBox from '@ts/ui/text_box/text_box'; import type Popover from '../popover/m_popover'; @@ -91,6 +92,16 @@ export interface DropDownEditorProperties extends Omit< _onMarkupRendered?: () => void; onPopupInitialized?: (e: { component: DropDownEditor; popup: Popup }) => void; + + popupPosition?: PositionConfig; + + useHiddenSubmitElement?: boolean; + + applyButtonText?: string; + + cancelButtonText?: string; + + validationMessagePosition?: Position | Mode; } interface TemplateRenderPayload { @@ -99,17 +110,24 @@ interface TemplateRenderPayload { onRendered?: () => void; } +interface FieldTemplate { + render: (payload: TemplateRenderPayload) => void +} + interface FieldAddonsTemplates { - beforeTemplate?: { render: (payload: TemplateRenderPayload) => void }; - afterTemplate?: { render: (payload: TemplateRenderPayload) => void }; + beforeTemplate?: FieldTemplate; + afterTemplate?: FieldTemplate; } +type PopupToolbarItemConfig = ToolbarItem & { shortcut?: string }; + function createTemplateWrapperElement(): dxElementWrapper { return $('
').addClass(DROP_DOWN_EDITOR_FIELD_TEMPLATE_WRAPPER); } class DropDownEditor< TProperties extends DropDownEditorProperties = DropDownEditorProperties, +// @ts-expect-error validationMessagePosition: Position | Mode vs TextBoxProperties (Position) > extends TextBox { _$container!: dxElementWrapper; @@ -144,11 +162,11 @@ class DropDownEditor< return { ...super._supportedKeys(), tab: (e): void => { - if (!this.option('opened')) { + const { opened } = this.option(); + if (!opened) { return; } - // @ts-expect-error ts-error - if (!this._popup.getFocusableElements().length) { + if (!this._popup?.getFocusableElements().length) { this.close(); return; } @@ -158,15 +176,16 @@ class DropDownEditor< : this._getFirstPopupElement(); if ($focusableElement) { - // @ts-expect-error ts-error + // @ts-expect-error should be added on EventsEngine level eventsEngine.trigger($focusableElement, 'focus'); - // @ts-expect-error ts-error + // @ts-expect-error should be added on dxElementWrapper level $focusableElement.select(); } e.preventDefault(); }, escape: (e): boolean => { - if (this.option('opened')) { + const { opened } = this.option(); + if (opened) { e.preventDefault(); } @@ -197,7 +216,8 @@ class DropDownEditor< return true; }, enter: (e): boolean => { - if (this.option('opened')) { + const { opened } = this.option(); + if (opened) { e.preventDefault(); this._valueChangeEventHandler(e); } @@ -207,7 +227,7 @@ class DropDownEditor< } _getDefaultButtons(): TextEditorButtonInfo[] { - // @ts-expect-error ts-error + // @ts-expect-error should be fixed on TextEditorButtonInfo level return super._getDefaultButtons().concat([{ name: 'dropDown', Ctor: DropDownButton }]); } @@ -251,11 +271,11 @@ class DropDownEditor< const position = getDefaultAlignment(isRtlEnabled); return { - // @ts-expect-error ts-error + // @ts-expect-error Should be updated on PositionConfig level offset: { h: 0, v: -1 }, my: `${position} top`, at: `${position} bottom`, - // @ts-expect-error ts-error + // @ts-expect-error Should be updated on PositionConfig level collision: 'flip flip', }; } @@ -267,7 +287,7 @@ class DropDownEditor< const isGeneric = device.platform === 'generic'; return isGeneric; }, - // @ts-expect-error ts-error + // @ts-expect-error Should be updated on PositionConfig level options: { popupPosition: { offset: { v: 0 } }, }, @@ -292,7 +312,7 @@ class DropDownEditor< _updatePopupPosition(isRtlEnabled?: boolean): void { const { my, at } = this._getDefaultPopupPosition(isRtlEnabled); - const currentPosition = this.option('popupPosition'); + const { popupPosition: currentPosition } = this.option(); this.option('popupPosition', extend({}, currentPosition, { my, at })); } @@ -335,7 +355,8 @@ class DropDownEditor< } _renderContentImpl(): void { - if (!this.option('deferRendering')) { + const { deferRendering } = this.option(); + if (!deferRendering) { this._createPopup(); } } @@ -392,13 +413,14 @@ class DropDownEditor< } } - _getFieldTemplate() { + _getFieldTemplate(): FieldTemplate | undefined { const { fieldTemplate } = this.option(); if (!fieldTemplate) { return; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, consistent-return return this._getTemplate(fieldTemplate); } @@ -437,7 +459,8 @@ class DropDownEditor< } _renderValue(): DeferredObj { - if (this.option('useHiddenSubmitElement')) { + const { useHiddenSubmitElement } = this.option(); + if (useHiddenSubmitElement) { this._setSubmitValue(); } @@ -492,13 +515,11 @@ class DropDownEditor< return; } - if (!this._$templateWrapper) { - this._$templateWrapper = createTemplateWrapperElement() - .prependTo(this.$element()); - } + this._$templateWrapper ??= createTemplateWrapperElement() + .prependTo(this.$element()); } - _renderTemplatedField(fieldTemplate, data): void { + _renderTemplatedField(fieldTemplate: FieldTemplate, data: TemplateRenderPayload): void { const isFocused = focused(this._input()); this._detachKeyboardEvents(); @@ -538,7 +559,7 @@ class DropDownEditor< const inputElement = $input.get(0) as HTMLInputElement; inputElement.focus({ preventScroll: true }); } else { - // @ts-expect-error + // @ts-expect-error should be added on EventsEngine level eventsEngine.trigger($input, 'focus'); } }, @@ -632,7 +653,7 @@ class DropDownEditor< _initTemplates(): void { this._templateManager.addDefaultTemplates({ - // @ts-expect-error ts-error + // @ts-expect-error should be fixed in FunctionTemplate definition dropDownButton: new FunctionTemplate((options) => { const $icon = $('
').addClass(DROP_DOWN_EDITOR_BUTTON_ICON); $(options.container).append($icon); @@ -643,7 +664,7 @@ class DropDownEditor< _renderOpenHandler(): void { const $inputWrapper = this._inputWrapper(); - // @ts-expect-error ts-error + // @ts-expect-error NAME property is missing const eventName = addNamespace(clickEventName, this.NAME); const { openOnFieldClick } = this.option(); @@ -659,10 +680,11 @@ class DropDownEditor< _attachFocusOutHandler(): void { if (isIOs) { this._detachFocusOutEvents(); - // @ts-expect-error ts-error - eventsEngine.on(this._inputWrapper(), addNamespace('focusout', this.NAME), (event) => { + // @ts-expect-error NAME property is missing + eventsEngine.on(this._inputWrapper(), addNamespace('focusout', this.NAME), (event: FocusEvent) => { const newTarget = event.relatedTarget; - if (newTarget && this.option('opened')) { + const { opened } = this.option(); + if (newTarget && opened) { const isNewTargetOutside = this._isTargetOutOfComponent(newTarget); if (isNewTargetOutside) { this.close(); @@ -672,45 +694,51 @@ class DropDownEditor< } } - _isTargetOutOfComponent(newTarget): boolean { + _isTargetOutOfComponent(newTarget: EventTarget | null): boolean { const popupWrapper = this.content ? $(this.content()).closest(`.${DROP_DOWN_EDITOR_OVERLAY}`) : this._$popup; - // @ts-expect-error + // @ts-expect-error Should be fixed on core/renderer level const isTargetOutsidePopup = $(newTarget).closest(`.${DROP_DOWN_EDITOR_OVERLAY}`, popupWrapper).length === 0; return isTargetOutsidePopup; } _detachFocusOutEvents(): void { - // @ts-expect-error ts-error - isIOs && eventsEngine.off(this._inputWrapper(), addNamespace('focusout', this.NAME)); + if (!isIOs) { + return; + } + // @ts-expect-error NAME property is missing + eventsEngine.off(this._inputWrapper(), addNamespace('focusout', this.NAME)); } - _getInputClickHandler(openOnFieldClick) { + _getInputClickHandler(openOnFieldClick?: boolean): ( + e: DxEvent, + ) => void { return openOnFieldClick - ? (e) => { this._executeOpenAction(e); } - : () => { this._focusInput(); }; + ? (e: DxEvent): void => { this._executeOpenAction(e); } + : (): void => { this._focusInput(); }; } _openHandler(): void { this._toggleOpenState(); } - _executeOpenAction(e): void { + _executeOpenAction(e: DxEvent): void { this._openOnFieldClickAction?.({ event: e }); } - _keyboardEventBindingTarget() { + _keyboardEventBindingTarget(): dxElementWrapper { return this._input(); } _focusInput(): boolean { - if (this.option('disabled')) { + const { disabled, focusStateEnabled } = this.option(); + if (disabled) { return false; } - if (this.option('focusStateEnabled') && !focused(this._input())) { + if (focusStateEnabled && !focused(this._input())) { this._resetCaretPosition(); - // @ts-expect-error ts-error + // @ts-expect-error should be added on EventsEngine level eventsEngine.trigger(this._input(), 'focus'); } @@ -721,8 +749,7 @@ class DropDownEditor< const inputElement = this._input().get(0); if (inputElement) { - // @ts-expect-error ts-error - const { value } = inputElement; + const { value } = inputElement as HTMLInputElement; const caretPosition = isDefined(value) && (ignoreEditable || this._isEditable()) ? value.length : 0; @@ -741,9 +768,10 @@ class DropDownEditor< return; } - if (!this.option('readOnly')) { - isVisible = arguments.length ? isVisible : !this.option('opened'); - this.option('opened', isVisible); + const { readOnly, opened } = this.option(); + + if (!readOnly) { + this.option('opened', isVisible ?? !opened); } } @@ -752,12 +780,12 @@ class DropDownEditor< } _renderOpenedState(): void { - const opened = this.option('opened'); + const { opened } = this.option(); if (opened) { this._createPopup(); } - // @ts-expect-error ts-error + this.$element().toggleClass(DROP_DOWN_EDITOR_ACTIVE, opened); this._setPopupOption('visible', opened); @@ -767,6 +795,7 @@ class DropDownEditor< }; this.setAria(arias); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing this.setAria('owns', (opened || undefined) && this._popupContentId, this.$element()); } @@ -784,8 +813,7 @@ class DropDownEditor< } _setPopupAriaLabel(): void { - // @ts-expect-error ts-error - const $overlayContent = this._popup.$overlayContent(); + const $overlayContent = this._popup?.$overlayContent(); this.setAria('label', OVERLAY_CONTENT_LABEL, $overlayContent); } @@ -798,8 +826,7 @@ class DropDownEditor< const cachedOptions = this._options.cache('dropDownOptions'); const popupConfig = extend(defaultOptions, cachedOptions); - // @ts-expect-error ts-error - this._popup = this._createComponent(this._$popup, Popup, popupConfig); + this._popup = this._createComponent(this._$popup as dxElementWrapper, Popup, popupConfig); this._popup.on({ showing: this._popupShowingHandler.bind(this), @@ -816,11 +843,11 @@ class DropDownEditor< } _attachPopupKeyHandler(): void { - // @ts-expect-error ts-error - eventsEngine.on(this._popup.$overlayContent(), addNamespace('keydown', this.NAME), (e) => this._popupKeyHandler(e)); + // @ts-expect-error ts-error NAME property is missing + eventsEngine.on(this._popup?.$overlayContent(), addNamespace('keydown', this.NAME), (e: DxEvent) => this._popupKeyHandler(e)); } - _popupKeyHandler(e): void { + _popupKeyHandler(e: DxEvent): void { // eslint-disable-next-line default-case, @typescript-eslint/switch-exhaustiveness-check switch (normalizeKeyName(e)) { case 'tab': @@ -832,20 +859,20 @@ class DropDownEditor< } } - _popupTabHandler(e): void { - const $target = $(e.target); + _popupTabHandler(e: DxEvent): void { + const $target = $(e.target as Element); const moveBackward = e.shiftKey && $target.is(this._getFirstPopupElement()); const moveForward = !e.shiftKey && $target.is(this._getLastPopupElement()); if (moveForward || moveBackward) { - // @ts-expect-error ts-error + // @ts-expect-error should be added on EventsEngine level eventsEngine.trigger(this.field(), 'focus'); e.preventDefault(); } } _popupEscHandler(): void { - // @ts-expect-error ts-error + // @ts-expect-error should be added on EventsEngine level eventsEngine.trigger(this._input(), 'focus'); this.close(); } @@ -859,13 +886,13 @@ class DropDownEditor< _contentReadyHandler(): void {} _popupConfig(): PopupProperties { + const { popupPosition, dropDownOptions } = this.option(); const config: PopupProperties = { onInitialized: this._getPopupInitializedHandler(), - position: extend(this.option('popupPosition'), { + position: extend(popupPosition, { of: this.$element(), }), - // @ts-expect-error ts-error - showTitle: this.option('dropDownOptions.showTitle'), + showTitle: dropDownOptions?.showTitle, width: getElementWidth(this.$element()), height: 'auto', shading: false, @@ -888,7 +915,7 @@ class DropDownEditor< toolbarItems: this._getPopupToolbarItems(), onPositioned: this._popupPositionedHandler.bind(this), fullScreen: false, - // @ts-expect-error ts-error + // @ts-expect-error should be added on Popup level contentTemplate: null, _hideOnParentScrollTarget: this.$element(), _wrapperClassExternal: DROP_DOWN_EDITOR_OVERLAY, @@ -901,14 +928,14 @@ class DropDownEditor< // eslint-disable-next-line class-methods-use-this _popupInitializedHandler(): void {} - _getPopupInitializedHandler(): (e) => void { + _getPopupInitializedHandler(): (e: PopupInitializedEvent) => void { const { onPopupInitialized } = this.option(); - return (e) => { + return (e: PopupInitializedEvent) => { this._popupInitializedHandler(); if (onPopupInitialized) { - // @ts-expect-error - this._popupInitializedAction({ popup: e.component }); + // @ts-expect-error action wrapper adds component/element at runtime + this._popupInitializedAction({ popup: e.component as Popup }); } }; } @@ -932,7 +959,7 @@ class DropDownEditor< } } - _popupPositionedHandler(e): void { + _popupPositionedHandler(e: Partial): void { const { labelMode, stylingMode } = this.option(); if (!this._popup) { @@ -951,9 +978,7 @@ class DropDownEditor< const $label = this._label.$element(); move($popupOverlayContent, { - // @ts-expect-error ts-error - // eslint-disable-next-line radix - top: locate($popupOverlayContent).top - parseInt($label.css('fontSize')), + top: locate($popupOverlayContent).top - parseInt($label.css('fontSize') ?? '0', 10), }); } } @@ -977,10 +1002,9 @@ class DropDownEditor< } _getValidationMessagePositionSide(): string { - // @ts-expect-error ts-error const { validationMessagePosition } = this.option(); - if (validationMessagePosition !== 'auto') { + if (validationMessagePosition && validationMessagePosition !== 'auto') { return validationMessagePosition; } @@ -990,8 +1014,9 @@ class DropDownEditor< const { top: myTop } = animationPosition.setup(this.$element()); const { top: popupTop } = animationPosition.setup(this._popup.$content()); - // @ts-expect-error ts-error - positionSide = (myTop + this.option('popupPosition').offset.v) > popupTop ? 'bottom' : 'top'; + const { popupPosition } = this.option(); + // @ts-expect-error Should be updated on PositionConfig level + positionSide = (myTop + popupPosition.offset.v) > popupTop ? 'bottom' : 'top'; } return positionSide; @@ -1002,10 +1027,9 @@ class DropDownEditor< const $target = $(target); const dropDownButton = this.getButton('dropDown'); - const $dropDownButton = dropDownButton?.$element(); - const isInputClicked = !!$target.closest(this.$element()).length; - // @ts-expect-error ts-error - const isDropDownButtonClicked = !!$target.closest($dropDownButton).length; + const $dropDownButton = dropDownButton?.$element() as dxElementWrapper; + const isInputClicked = Boolean($target.closest(this.$element()).length); + const isDropDownButtonClicked = Boolean($target.closest($dropDownButton).length); const isOutsideClick = !isInputClicked && !isDropDownButtonClicked; return isOutsideClick; @@ -1024,19 +1048,19 @@ class DropDownEditor< super._clean(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _setPopupOption(optionName: string, value?): void { - // @ts-expect-error ts-error - this._setWidgetOption('_popup', arguments); + _setPopupOption(...args: [string, unknown?]): void { + // @ts-expect-error Should be fixed on Widget level + this._setWidgetOption('_popup', args); } _validatedOpening(): void { - if (!this.option('readOnly')) { + const { readOnly } = this.option(); + if (!readOnly) { this._toggleOpenState(true); } } - _getPopupToolbarItems() { + _getPopupToolbarItems(): ToolbarItem[] { const { applyValueMode } = this.option(); return applyValueMode === 'useButtons' @@ -1045,29 +1069,28 @@ class DropDownEditor< } _getFirstPopupElement(): dxElementWrapper { - // @ts-expect-error ts-error - return $(this._popup.getFocusableElements()).first(); + return $(this._popup?.getFocusableElements()).first(); } _getLastPopupElement(): dxElementWrapper { - // @ts-expect-error ts-error - return $(this._popup.getFocusableElements()).last(); + return $(this._popup?.getFocusableElements()).last(); } - _popupToolbarItemsConfig() { + _popupToolbarItemsConfig(): PopupToolbarItemConfig[] { + const { applyButtonText, cancelButtonText } = this.option(); const buttonsConfig = [ { shortcut: 'done', options: { onClick: this._applyButtonHandler.bind(this), - text: this.option('applyButtonText'), + text: applyButtonText, }, }, { shortcut: 'cancel', options: { onClick: this._cancelButtonHandler.bind(this), - text: this.option('cancelButtonText'), + text: cancelButtonText, }, }, ]; @@ -1075,14 +1098,14 @@ class DropDownEditor< return this._applyButtonsLocation(buttonsConfig); } - _applyButtonsLocation(buttonsConfig) { + _applyButtonsLocation(buttonsConfig: PopupToolbarItemConfig[]): PopupToolbarItemConfig[] { const { buttonsLocation } = this.option(); const resultConfig = buttonsConfig; if (buttonsLocation !== 'default') { const position = splitPair(buttonsLocation); - each(resultConfig, (_, element) => { + resultConfig.forEach((element) => { extend(element, { toolbar: position[0], location: position[1], @@ -1094,23 +1117,25 @@ class DropDownEditor< } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _applyButtonHandler(args?): void { + _applyButtonHandler(e?: unknown): void { this.close(); - if (this.option('focusStateEnabled')) { + const { focusStateEnabled } = this.option(); + if (focusStateEnabled) { this.focus(); } } _cancelButtonHandler(): void { this.close(); - if (this.option('focusStateEnabled')) { + const { focusStateEnabled } = this.option(); + if (focusStateEnabled) { this.focus(); } } - _popupOptionChanged(args): void { - // @ts-expect-error ts-error + _popupOptionChanged(args: OptionChanged): void { + // @ts-expect-error Add getOptionsFromContainer static method to Widget const options = Widget.getOptionsFromContainer(args); this._setPopupOption(options); @@ -1123,7 +1148,8 @@ class DropDownEditor< } _renderSubmitElement(): void { - if (this.option('useHiddenSubmitElement')) { + const { useHiddenSubmitElement } = this.option(); + if (useHiddenSubmitElement) { this._$submitElement = $('') .attr('type', 'hidden') .appendTo(this.$element()); @@ -1137,10 +1163,11 @@ class DropDownEditor< } _getSubmitElement(): dxElementWrapper { - if (this.option('useHiddenSubmitElement')) { - // @ts-expect-error ts-error - return this._$submitElement; + const { useHiddenSubmitElement } = this.option(); + if (useHiddenSubmitElement) { + return this._$submitElement as dxElementWrapper; } + return super._getSubmitElement(); } diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts index 576fd0c47a2e..3013687cb367 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_utils.ts @@ -2,21 +2,24 @@ import type { dxElementWrapper } from '@js/core/renderer'; import { getOuterWidth } from '@js/core/utils/size'; import { hasWindow } from '@js/core/utils/window'; -const getElementWidth = function ($element: dxElementWrapper) { +type SizeValue = number | string | (() => number | string) | null | undefined; + +const getElementWidth = ($element: dxElementWrapper): number | undefined => { if (hasWindow()) { - return getOuterWidth($element); + return getOuterWidth($element) as number; } + + return undefined; }; -const getSizeValue = function (size) { - if (size === null) { - size = undefined; - } - if (typeof size === 'function') { - size = size(); +const getSizeValue = (size: SizeValue): number | string | undefined => { + const normalized = size === null ? undefined : size; + + if (typeof normalized === 'function') { + return normalized(); } - return size; + return normalized; }; export { getElementWidth, getSizeValue }; diff --git a/packages/devextreme/js/__internal/ui/m_lookup.ts b/packages/devextreme/js/__internal/ui/m_lookup.ts index 8e9e14b577ed..855f8acb14a9 100644 --- a/packages/devextreme/js/__internal/ui/m_lookup.ts +++ b/packages/devextreme/js/__internal/ui/m_lookup.ts @@ -1161,13 +1161,12 @@ class Lookup extends DropDownList { switch (fullName) { case 'dropDownOptions.width': case 'dropDownOptions.height': { - const args = { - name, - fullName, + const optionArgs = { + ...args, value: value === 'auto' ? this.initialOption('dropDownOptions')[getFieldName(fullName)] : value, }; - this._popupOptionChanged(args); - this._innerWidgetOptionChanged(this._popup, args); + this._popupOptionChanged(optionArgs); + this._innerWidgetOptionChanged(this._popup, optionArgs); break; } default: From 39a217ec1870e02ad479f3547727382d35cb24ee Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Tue, 2 Jun 2026 18:35:15 +0300 Subject: [PATCH 2/4] DropDownList: Refactor types --- .../ui/drop_down_editor/m_drop_down_list.ts | 446 ++++++++++-------- .../js/__internal/ui/m_autocomplete.ts | 8 +- .../js/__internal/ui/m_select_box.ts | 15 +- .../devextreme/js/__internal/ui/m_tag_box.ts | 7 +- 4 files changed, 267 insertions(+), 209 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts index 0428d7af05f1..e03432843844 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts @@ -2,7 +2,7 @@ import type { SingleMultipleAllOrNone } from '@js/common'; import eventsEngine from '@js/common/core/events/core/events_engine'; import { addNamespace } from '@js/common/core/events/utils'; import messageLocalization from '@js/common/core/localization/message'; -import type { GroupItem } from '@js/common/data'; +import type { DataSource, GroupItem } from '@js/common/data'; import dataQuery from '@js/common/data/query'; import registerComponent from '@js/core/component_registrator'; import devices from '@js/core/devices'; @@ -13,24 +13,24 @@ import $ from '@js/core/renderer'; import { ChildDefaultTemplate } from '@js/core/templates/child_default_template'; import { ensureDefined, - // @ts-expect-error ts-error + // @ts-expect-error add export grep, noop, } from '@js/core/utils/common'; -import { Deferred } from '@js/core/utils/deferred'; +import { Deferred, type DeferredObj } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; -import { each } from '@js/core/utils/iterator'; import { getOuterHeight } from '@js/core/utils/size'; import { isDefined, isObject, isWindow } from '@js/core/utils/type'; import { getWindow } from '@js/core/utils/window'; import type { DataSourceLike, DataSourceOptions } from '@js/data/data_source'; import type { dxDropDownListOptions } from '@js/ui/drop_down_editor/ui.drop_down_list'; import DataExpressionMixin from '@js/ui/editor/ui.data_expression'; -import type { Item } from '@js/ui/list'; +import type { Item, ItemClickEvent } from '@js/ui/list'; import type { Properties as PopupProperties } from '@js/ui/popup'; import errors from '@js/ui/widget/ui.errors'; import type { OptionChanged } from '@ts/core/widget/types'; import { getDataSourceOptions } from '@ts/data/data_converter/grouped'; +import type DataController from '@ts/ui/collection/m_data_controller'; import DropDownEditor from '@ts/ui/drop_down_editor/m_drop_down_editor'; import type { ListBaseProperties } from '@ts/ui/list/list.base'; import List from '@ts/ui/list/list.edit.search'; @@ -47,11 +47,15 @@ const SEARCH_MODES = ['startswith', 'contains', 'endwith', 'notcontains']; const useCompositionEvents = devices.real().platform !== 'android'; +export interface ItemCache { itemByValue?: Record } + interface DropDownListProperties extends Omit, 'onOpened' | 'onClosed' | 'onChange' | 'onCopy' | 'onCut' | 'onEnterKey' | 'onFocusIn' | 'onFocusOut' | 'onInput' | 'onKeyDown' | 'onKeyUp' | 'onPaste' | 'onValueChanged' | 'validationMessagePosition' | 'onContentReady' | 'onDisposing' | 'onOptionChanged' | 'onInitialized'> { encodeNoDataText?: boolean; + displayCustomValue?: boolean; + items?: Item[]; } class DropDownList< @@ -69,15 +73,15 @@ class DropDownList< _selectionChangedAction!: (event?: Record) => void; - _itemClickAction!: (event?: Record) => void; + _itemClickAction!: (event?: ItemClickEvent) => void; _$customBoundaryContainer?: dxElementWrapper; _pageIndex?: number; - _dataController?: any; + _dataController!: DataController; - _dataSource?: any; + _dataSource!: DataSource; _isTextComposition?: boolean; @@ -110,20 +114,20 @@ class DropDownList< return opened && applyValueMode === 'instantly'; } - _setSelectedElement($element): void { - // @ts-expect-error ts-error + _setSelectedElement($element: dxElementWrapper): void { + // @ts-expect-error refactor DataExpressionMixin const value = this._valueGetter(this._list._getItemData($element)); this._setValue(value); } - _setValue(value): void { + _setValue(value: unknown): void { this.option('value', value); } _getDefaultOptions(): TProperties { return { ...super._getDefaultOptions(), - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin ...DataExpressionMixin._dataExpressionDefaultOptions(), displayValue: undefined, searchEnabled: false, @@ -152,7 +156,6 @@ class DropDownList< } _defaultOptionsRules(): DefaultOptionsRule[] { - // @ts-expect-error ts-error return super._defaultOptionsRules().concat([ { device: { platform: 'ios' }, @@ -166,7 +169,7 @@ class DropDownList< buttonsLocation: 'bottom center', }, }, - ]); + ] as DefaultOptionsRule[]); } _setOptionsByReference(): void { @@ -181,7 +184,7 @@ class DropDownList< _init(): void { super._init(); - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin this._initDataExpressions(); this._initActions(); this._setListDataSource(); @@ -191,7 +194,10 @@ class DropDownList< } _setListFocusedElementOptionChange(): void { - // @ts-expect-error ts-error + if (!this._list) { + return; + } + this._list._updateParentActiveDescendant = this._updateActiveDescendant.bind(this); } @@ -210,7 +216,7 @@ class DropDownList< } _initContentReadyAction(): void { - // @ts-expect-error + // @ts-expect-error _contentReadyAction not typed on base class this._contentReadyAction = this._createActionByOption('onContentReady', { excludeValidators: ['disabled', 'readOnly'], }); @@ -246,7 +252,8 @@ class DropDownList< } } - _fitIntoRange(value, start, end) { + // eslint-disable-next-line class-methods-use-this + _fitIntoRange(value: number, start: number, end: number): number { if (value > end) { return start; } @@ -256,78 +263,66 @@ class DropDownList< return value; } - _items() { + _items(): Item[] { const items = this._getPlainItems(!this._list && this._dataSource.items()); - // @ts-expect-error + // @ts-expect-error dataQuery is callable as a constructor // eslint-disable-next-line new-cap - const availableItems = new dataQuery(items).filter('disabled', '<>', true).toArray(); - - return availableItems; + return new dataQuery(items).filter('disabled', '<>', true).toArray() as Item[]; } - _calcNextItem(step) { + _calcNextItem(step: number): Item { const items = this._items(); const nextIndex = this._fitIntoRange(this._getSelectedIndex() + step, 0, items.length - 1); return items[nextIndex]; } - _getSelectedIndex() { + _getSelectedIndex(): number { const items = this._items(); - const selectedItem = this.option('selectedItem'); - let result = -1; - // @ts-expect-error - each(items, (index, item) => { - // @ts-expect-error ts-error - if (this._isValueEquals(item, selectedItem)) { - result = index; - return false; - } - }); - - return result; + const { selectedItem } = this.option(); + // @ts-expect-error refactor DataExpressionMixin + return items.findIndex((item) => this._isValueEquals(item, selectedItem)); } _createPopup(): void { super._createPopup(); this._updateCustomBoundaryContainer(); - // @ts-expect-error ts-error - this._popup.$wrapper().addClass(this._popupWrapperClass()); - // @ts-expect-error ts-error - const $popupContent = this._popup.$content(); + this._popup?.$wrapper()?.addClass(this._popupWrapperClass()); + const $popupContent = this._popup?.$content(); eventsEngine.off($popupContent, 'mouseup'); eventsEngine.on($popupContent, 'mouseup', this._saveFocusOnWidget.bind(this)); } _updateCustomBoundaryContainer(): void { - const customContainer = this.option('dropDownOptions.container'); - // @ts-expect-error ts-error - const $container = customContainer && $(customContainer); + const { dropDownOptions } = this.option(); + const customContainer = dropDownOptions?.container; + const $container = $(customContainer); + + if ($container.length && !isWindow($container.get(0))) { + const $containerWithParents: Element[] = [].slice.call($container.parents()); - if ($container && $container.length && !isWindow($container.get(0))) { - const $containerWithParents = [].slice.call($container.parents()); - // @ts-expect-error $containerWithParents.unshift($container.get(0)); - // @ts-expect-error - each($containerWithParents, (i, parent) => { - if (parent === $('body').get(0)) { - return false; - } if (window.getComputedStyle(parent).overflowY === 'hidden') { - this._$customBoundaryContainer = $(parent); - return false; - } - }); + const overflowParent = $containerWithParents.find( + (parent) => parent !== $('body').get(0) && window.getComputedStyle(parent).overflowY === 'hidden', + ); + + if (overflowParent) { + this._$customBoundaryContainer = $(overflowParent); + } } } + // eslint-disable-next-line class-methods-use-this _popupWrapperClass(): string { return DROPDOWNLIST_POPUP_WRAPPER_CLASS; } - _renderInputValue({ value, renderOnly }: { value?: unknown; renderOnly?: boolean } = {}) { - // @ts-expect-error ts-error + _renderInputValue( + { value, renderOnly }: { value?: unknown; renderOnly?: boolean } = {}, + ): DeferredObj { + // @ts-expect-error refactor DataExpressionMixin const currentValue = value ?? this._getCurrentValue(); - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin this._rejectValueLoading(); if (renderOnly) { @@ -337,37 +332,42 @@ class DropDownList< return this ._loadInputValue( currentValue, - // @ts-expect-error ts-error - (...args) => { this._setSelectedItem(...args); }, + (...args: [unknown]) => { this._setSelectedItem(...args as [Item]); }, ) + // eslint-disable-next-line @typescript-eslint/no-misused-promises .always(super._renderInputValue.bind(this, currentValue)); } - _loadInputValue(value, callback) { - // @ts-expect-error ts-error - return this._loadItem(value).always(callback); + _loadInputValue(value: unknown, callback: (...args: [unknown]) => void): DeferredObj { + return (this._loadItem(value) as DeferredObj).always(callback); } - _getItemFromPlain(value, cache?) { - let plainItems; - let selectedItem; + _getItemFromPlain(value: unknown, cache?: ItemCache): Item | undefined { + let plainItems: Item[] = []; + // eslint-disable-next-line @typescript-eslint/init-declarations + let selectedItem: Item | undefined; if (cache && typeof value !== 'object') { if (!cache.itemByValue) { cache.itemByValue = {}; plainItems = this._getPlainItems(); - plainItems.forEach(function (item) { - cache.itemByValue[this._valueGetter(item)] = item; + const { itemByValue } = cache; + plainItems.forEach((item) => { + // @ts-expect-error refactor DataExpressionMixin + itemByValue[this._valueGetter(item)] = item; }, this); } - selectedItem = cache.itemByValue[value]; + selectedItem = cache.itemByValue[value as PropertyKey]; } if (!selectedItem) { plainItems = this._getPlainItems(); - // @ts-expect-error ts-error // eslint-disable-next-line prefer-destructuring - selectedItem = grep(plainItems, (item) => this._isValueEquals(this._valueGetter(item), value))[0]; + selectedItem = grep( + plainItems, + // @ts-expect-error refactor DataExpressionMixin + (item: Item) => this._isValueEquals(this._valueGetter(item), value) as boolean, + )[0]; } return selectedItem; @@ -377,35 +377,31 @@ class DropDownList< this._renderInputValue({ renderOnly: true }); } - _loadItem(value, cache) { + _loadItem(value: unknown, cache?: ItemCache): DeferredObj | Promise { const selectedItem = this._getItemFromPlain(value, cache); - return selectedItem !== undefined - ? Deferred().resolve(selectedItem).promise() - // @ts-expect-error ts-error - : this._loadValue(value); - } - - _getPlainItems(items?) { - let plainItems: any = []; + if (selectedItem !== undefined) { + return Deferred().resolve(selectedItem); + } - const { grouped } = this.option(); + // @ts-expect-error refactor DataExpressionMixin + return this._loadValue(value) as DeferredObj; + } - items = items || this.option('items') || this._dataSource.items() || []; + _getPlainItems(inputItems?: Item[] | GroupItem[] | false): Item[] { + const { grouped, items: optionItems } = this.option(); + const items: (Item | GroupItem)[] = ( + Array.isArray(inputItems) ? inputItems : undefined + ) ?? optionItems ?? this._dataSource.items() ?? []; - for (let i = 0; i < items.length; i++) { - if (grouped && items[i]?.items) { - plainItems = plainItems.concat(items[i].items); - } else { - plainItems.push(items[i]); - } - } - - return plainItems; + return items.flatMap((item) => { + const groupedItem = item as GroupItem; + return (grouped && groupedItem.items) ? groupedItem.items as Item[] : [item as Item]; + }); } - _updateActiveDescendant($target?): void { - const opened = this.option('opened'); + _updateActiveDescendant($target?: dxElementWrapper): void { + const { opened } = this.option(); const listFocusedItemId = this._list?.getFocusedItemId(); const isElementOnDom = $(`#${listFocusedItemId}`).length > 0; const activedescendant = opened && isElementOnDom && listFocusedItemId; @@ -416,33 +412,32 @@ class DropDownList< }, $target); } - _setSelectedItem(item): void { + _setSelectedItem(item: Item): void { const displayValue = this._displayValue(item); this.option('selectedItem', ensureDefined(item, null)); this.option('displayValue', displayValue); } - _displayValue(item) { - // @ts-expect-error ts-error - return this._displayGetter(item); + _displayValue(item: Item): string { + // @ts-expect-error refactor DataExpressionMixin + return this._displayGetter(item) as string; } _refreshSelected(): void { - const cache = {}; - // @ts-expect-error ts-error - this._listItemElements().each((_, itemElement) => { + const cache: ItemCache = {}; + const elements = Array.from(this._listItemElements() as unknown as ArrayLike); + + elements.forEach((itemElement) => { const $itemElement = $(itemElement); - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin const itemValue = this._valueGetter($itemElement.data(LIST_ITEM_DATA_KEY)); const isItemSelected = this._isSelectedValue(itemValue, cache); if (isItemSelected) { - // @ts-expect-error ts-error - this._list.selectItem($itemElement); + this._list?.selectItem(itemElement); } else { - // @ts-expect-error ts-error - this._list.unselectItem($itemElement); + this._list?.unselectItem(itemElement); } }); } @@ -453,7 +448,9 @@ class DropDownList< } _setFocusPolicy(): void { - if (!this.option('focusStateEnabled') || !this._list) { + const { focusStateEnabled } = this.option(); + + if (!focusStateEnabled || !this._list) { return; } @@ -461,15 +458,21 @@ class DropDownList< } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _isSelectedValue(value, cache?) { - // @ts-expect-error ts-error - return this._isValueEquals(value, this.option('value')); + _isSelectedValue(value: unknown, cache?: ItemCache): boolean { + const { value: optionValue } = this.option(); + + // @ts-expect-error refacotor DataExpressionMixin + return this._isValueEquals(value, optionValue) as boolean; } _validateSearchMode(): void { - const searchMode = this.option('searchMode'); - // @ts-expect-error ts-error - const normalizedSearchMode = searchMode.toLowerCase(); + const { searchMode } = this.option(); + + if (!searchMode) { + return; + } + + const normalizedSearchMode = searchMode?.toLowerCase(); if (!SEARCH_MODES.includes(normalizedSearchMode)) { throw errors.Error('E1019', searchMode); @@ -481,7 +484,7 @@ class DropDownList< } _processDataSourceChanging(): void { - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin this._initDataController(); this._setListOption('_dataController', this._dataController); this._setListDataSource(); @@ -494,8 +497,10 @@ class DropDownList< }); } - _isCustomValueAllowed() { - return this.option('displayCustomValue'); + _isCustomValueAllowed(): boolean { + const { displayCustomValue } = this.option(); + + return Boolean(displayCustomValue); } clear(): void { @@ -526,7 +531,7 @@ class DropDownList< this._renderList(); } - _getKeyboardListeners(): any[] { + _getKeyboardListeners(): unknown[] { const canListHaveFocus = this._canListHaveFocus(); if (!canListHaveFocus) { @@ -537,13 +542,12 @@ class DropDownList< } _renderList(): void { - // @ts-expect-error + // @ts-expect-error Guid has no _value this._listId = `dx-${new Guid()._value}`; const $list = $('
') .attr('id', this._listId) - // @ts-expect-error ts-error - .appendTo(this._popup.$content()); + .appendTo(this._popup?.$content() as dxElementWrapper); this._$list = $list; this._list = this._createComponent($list, List, this._listConfig()); @@ -557,16 +561,18 @@ class DropDownList< const eventName = addNamespace('mousedown', 'dxDropDownList'); eventsEngine.off(this._$list, eventName); - eventsEngine.on(this._$list, eventName, (e) => e.preventDefault()); + eventsEngine.on(this._$list, eventName, (e: MouseEvent) => e.preventDefault()); } - _getControlsAria() { + _getControlsAria(): string | undefined { return this._list && this._listId; } _renderOpenedState(): void { super._renderOpenedState(); - this._list && this._updateActiveDescendant(); + if (this._list) { + this._updateActiveDescendant(); + } this.setAria('owns', this._popup && this._popupContentId); } @@ -582,12 +588,12 @@ class DropDownList< } _shouldRefreshDataSource(): boolean { - // @ts-expect-error ts-error - const dataSourceProvided = !!this._list.option('dataSource'); + const { dataSource } = this._list?.option() ?? {}; - return dataSourceProvided !== this._needPassDataSourceToList(); + return Boolean(dataSource) !== this._needPassDataSourceToList(); } + // eslint-disable-next-line class-methods-use-this _isDesktopDevice(): boolean { return devices.real().deviceType === 'desktop'; } @@ -617,9 +623,9 @@ class DropDownList< onContentReady: this._listContentReadyHandler.bind(this), itemTemplate, indicateLoading: false, - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin keyExpr: this._getCollectionKeyExpr(), - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin displayExpr: this._displayGetterExpr(), groupTemplate, onItemClick: this._listItemClickAction.bind(this), @@ -628,13 +634,12 @@ class DropDownList< hoverStateEnabled: this._isDesktopDevice() ? hoverStateEnabled : false, focusStateEnabled, _onItemsRendered: (): void => { - // @ts-expect-error ts-error - this._popup.repaint(); + this._popup?.repaint(); }, }; if (!this._canListHaveFocus()) { - // @ts-expect-error ts-error + // @ts-expect-error Fix on List level options.tabIndex = null; } @@ -646,11 +651,12 @@ class DropDownList< return false; } - _getDataSource() { + _getDataSource(): DataSourceLike | null { return this._needPassDataSourceToList() ? this._dataSource : null; } - _dataSourceOptions() { + // eslint-disable-next-line class-methods-use-this + _dataSourceOptions(): Partial> { return { paginate: false, }; @@ -660,8 +666,7 @@ class DropDownList< | DataSourceOptions> | null | undefined { - const { grouped } = this.option(); - const dataSource = this.option('dataSource'); + const { grouped, dataSource } = this.option(); if (dataSource && grouped) { return getDataSourceOptions(dataSource); @@ -670,38 +675,45 @@ class DropDownList< return dataSource; } + // eslint-disable-next-line class-methods-use-this _dataSourceFromUrlLoadMode(): string { return 'raw'; } _listContentReadyHandler(): void { - // @ts-expect-error ts-error - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this._list = this._list || this._$list.dxList('instance'); + if (!this._$list) { + return; + } + + const { deferRendering } = this.option(); - if (!this.option('deferRendering')) { + this._list = List.getInstance(this._$list); + + if (!deferRendering) { this._refreshSelected(); } this._updatePopupWidth(); this._updateListDimensions(); - // @ts-expect-error - this._contentReadyAction(); + this._contentReadyAction?.(); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _setListOption(optionName, value?): void { - // @ts-expect-error ts-error - this._setWidgetOption('_list', arguments); + _setListOption( + optionName: K, value?: ListBaseProperties[K] + ): void; + _setListOption(optionName: string, value?: unknown): void; + _setListOption(...args: [string, unknown?]): void { + // @ts-expect-error fix on Widget level + this._setWidgetOption('_list', args); } - _listItemClickAction(e): void { + _listItemClickAction(e: ItemClickEvent): void { this._listItemClickHandler(e); this._itemClickAction(e); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _listItemClickHandler(e?): void { } + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + _listItemClickHandler(e?: ItemClickEvent): void { } _setListDataSource(): void { if (!this._list) { @@ -723,9 +735,9 @@ class DropDownList< } _isMinSearchLengthExceeded(): boolean { - // @ts-expect-error ts-error - // eslint-disable-next-line @typescript-eslint/no-base-to-string - return this._searchValue().toString().length >= this.option('minSearchLength'); + const { minSearchLength } = this.option(); + + return this._searchValue().toString().length >= (minSearchLength ?? 0); } _needClearFilter(): boolean { @@ -734,52 +746,71 @@ class DropDownList< _canKeepDataSource(): boolean { const isMinSearchLengthExceeded = this._isMinSearchLengthExceeded(); - return this._dataController.isLoaded() - && this.option('showDataBeforeSearch') - && this.option('minSearchLength') + const { showDataBeforeSearch, minSearchLength } = this.option(); + + return this._dataController.isLoaded() as boolean + && Boolean(showDataBeforeSearch) + && Boolean(minSearchLength) && !isMinSearchLengthExceeded && !this._isLastMinSearchLengthExceeded; } - _searchValue() { + _searchValue(): string { return this._input().val() || ''; } - _getSearchEvent() { + _getSearchEvent(): string { return addNamespace(SEARCH_EVENT, `${this.NAME}Search`); } - _getCompositionStartEvent() { + _getCompositionStartEvent(): string { return addNamespace('compositionstart', `${this.NAME}CompositionStart`); } - _getCompositionEndEvent() { + _getCompositionEndEvent(): string { return addNamespace('compositionend', `${this.NAME}CompositionEnd`); } - _getSetFocusPolicyEvent() { + _getSetFocusPolicyEvent(): string { return addNamespace('input', `${this.NAME}FocusPolicy`); } _renderEvents(): void { super._renderEvents(); - eventsEngine.on(this._input(), this._getSetFocusPolicyEvent(), () => { this._setFocusPolicy(); }); + eventsEngine.on( + this._input(), + this._getSetFocusPolicyEvent(), + () => { this._setFocusPolicy(); }, + ); if (this._shouldRenderSearchEvent()) { - eventsEngine.on(this._input(), this._getSearchEvent(), (e) => { this._searchHandler(e); }); + eventsEngine.on( + this._input(), + this._getSearchEvent(), + (e: InputEvent) => { this._searchHandler(e); }, + ); if (useCompositionEvents) { - eventsEngine.on(this._input(), this._getCompositionStartEvent(), () => { this._isTextCompositionInProgress(true); }); - eventsEngine.on(this._input(), this._getCompositionEndEvent(), (e) => { - this._isTextCompositionInProgress(undefined); - this._searchHandler(e, this._searchValue()); - }); + eventsEngine.on( + this._input(), + this._getCompositionStartEvent(), + () => { this._isTextCompositionInProgress(true); }, + ); + eventsEngine.on( + this._input(), + this._getCompositionEndEvent(), + (e: CompositionEvent) => { + this._isTextCompositionInProgress(undefined); + this._searchHandler(e, this._searchValue()); + }, + ); } } } _shouldRenderSearchEvent(): boolean | undefined { - // @ts-expect-error ts-error - return this.option('searchEnabled'); + const { searchEnabled } = this.option(); + + return searchEnabled; } _refreshEvents(): void { @@ -793,17 +824,17 @@ class DropDownList< super._refreshEvents(); } - // @ts-expect-error ts-error - // eslint-disable-next-line consistent-return - _isTextCompositionInProgress(value?: boolean) { + _isTextCompositionInProgress(value?: boolean): boolean | undefined { if (arguments.length) { this._isTextComposition = value; } else { return this._isTextComposition; } + + return undefined; } - _searchHandler(e, searchValue?): void { + _searchHandler(e?: InputEvent | CompositionEvent, searchValue?: string): void { if (this._isTextCompositionInProgress()) { return; } @@ -817,6 +848,7 @@ class DropDownList< if (searchTimeout) { this._clearSearchTimer(); + // eslint-disable-next-line no-restricted-globals this._searchTimer = setTimeout( () => { this._searchDataSource(searchValue); }, searchTimeout, @@ -838,12 +870,13 @@ class DropDownList< this._filterDataSource(searchValue); } - _filterDataSource(searchValue): void { + _filterDataSource(searchValue: string | null): void { + const { searchExpr } = this.option(); this._clearSearchTimer(); const dataController = this._dataController; - // @ts-expect-error ts-error - dataController.searchExpr(this.option('searchExpr') || this._displayGetterExpr()); + // @ts-expect-error refactor DataExpressionMixin + dataController.searchExpr(searchExpr ?? this._displayGetterExpr()); dataController.searchOperation(this.option('searchMode')); dataController.searchValue(searchValue); dataController.load().done(this._dataSourceFiltered.bind(this, searchValue)); @@ -851,11 +884,15 @@ class DropDownList< _clearFilter(): void { const dataController = this._dataController; - dataController.searchValue() && dataController.searchValue(null); + + // @ts-expect-error fix argument type in m_data_controller.ts + if (dataController.searchValue()) { + dataController.searchValue(null); + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars - _dataSourceFiltered(searchValue?): void { + _dataSourceFiltered(searchValue?: string | null): void { this._isLastMinSearchLengthExceeded = this._isMinSearchLengthExceeded(); this._refreshList(); this._refreshPopupVisibility(); @@ -866,7 +903,9 @@ class DropDownList< } _refreshPopupVisibility(): void { - if (this.option('readOnly') || !this._searchValue()) { + const { readOnly } = this.option(); + + if (readOnly || !this._searchValue()) { return; } @@ -884,18 +923,17 @@ class DropDownList< } } - _dataSourceChangedHandler(newItems): void { + _dataSourceChangedHandler(newItems: Item[]): void { if (this._dataController.pageIndex() === 0) { this.option().items = newItems; } else { - // @ts-expect-error ts-error - this.option().items = this.option().items.concat(newItems); + this.option().items = this.option().items?.concat(newItems); } } _hasItemsToShow(): boolean { const dataController = this._dataController; - const resultItems = dataController.items() || []; + const resultItems = dataController.items() ?? []; const resultAmount = resultItems.length; const isMinSearchLengthExceeded = this._needPassDataSourceToList(); @@ -933,8 +971,7 @@ class DropDownList< const dataController = this._dataController; const currentPageIndex = dataController.pageIndex(); const needRepaint = (isDefined(this._pageIndex) && currentPageIndex <= this._pageIndex) - // @ts-expect-error ts-error - || (dataController.isLastPage() && !this._list._scrollViewIsFull()); + || (dataController.isLastPage() as boolean && !this._list?._scrollViewIsFull()); this._pageIndex = currentPageIndex; @@ -951,6 +988,7 @@ class DropDownList< } if (this._list) { + // eslint-disable-next-line @typescript-eslint/no-floating-promises this._list.updateDimensions(); } } @@ -958,10 +996,17 @@ class DropDownList< _getMaxHeight(): number { const $element = this.$element(); const $customBoundaryContainer = this._$customBoundaryContainer; - // @ts-expect-error ts-error - const offsetTop = $element.offset().top - ($customBoundaryContainer ? $customBoundaryContainer.offset().top : 0); + + const offsetTop = ($element.offset()?.top ?? 0) - ( + $customBoundaryContainer ? $customBoundaryContainer.offset()?.top ?? 0 : 0 + ); + const windowHeight = getOuterHeight(window); - const containerHeight = $customBoundaryContainer ? Math.min(getOuterHeight($customBoundaryContainer), windowHeight) : windowHeight; + + const containerHeight = $customBoundaryContainer + ? Math.min(getOuterHeight($customBoundaryContainer), windowHeight) + : windowHeight; + const maxHeight = Math.max(offsetTop, containerHeight - offsetTop - getOuterHeight($element)); return Math.min(containerHeight * 0.5, maxHeight); @@ -982,30 +1027,31 @@ class DropDownList< super._dispose(); } - _setCollectionWidgetOption(): void { - // @ts-expect-error ts-error - this._setListOption.apply(this, arguments); + _setCollectionWidgetOption(...args: Parameters): void { + this._setListOption(...args); } _setSubmitValue(): void { - const value = this.option('value'); - // @ts-expect-error ts-error + const { value } = this.option(); + // @ts-expect-error refactor DataExpressionMixin const submitValue = this._shouldUseDisplayValue(value) ? this._displayGetter(value) : value; this._getSubmitElement().val(submitValue); } - _shouldUseDisplayValue(value): boolean { - // @ts-expect-error ts-error - return this.option('valueExpr') === 'this' && isObject(value); + _shouldUseDisplayValue(value: unknown): boolean { + const { valueExpr } = this.option(); + return valueExpr === 'this' && isObject(value); } _optionChanged(args: OptionChanged): void { - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin this._dataExpressionOptionChanged(args); switch (args.name) { case 'hoverStateEnabled': - this._isDesktopDevice() && this._setListOption(args.name, args.value); + if (this._isDesktopDevice()) { + this._setListOption(args.name, args.value); + } super._optionChanged(args); break; case 'focusStateEnabled': @@ -1022,12 +1068,12 @@ class DropDownList< break; case 'valueExpr': this._renderValue(); - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin this._setListOption('keyExpr', this._getCollectionKeyExpr()); break; case 'displayExpr': this._renderValue(); - // @ts-expect-error ts-error + // @ts-expect-error refactor DataExpressionMixin this._setListOption('displayExpr', this._displayGetterExpr()); break; case 'searchMode': @@ -1075,7 +1121,7 @@ class DropDownList< } } -// @ts-expect-error ts-error +// @ts-expect-error refactor DataExpressionMixin DropDownList.include(DataExpressionMixin); registerComponent('dxDropDownList', DropDownList); diff --git a/packages/devextreme/js/__internal/ui/m_autocomplete.ts b/packages/devextreme/js/__internal/ui/m_autocomplete.ts index 84a5824b5814..d7ac5d3b7db8 100644 --- a/packages/devextreme/js/__internal/ui/m_autocomplete.ts +++ b/packages/devextreme/js/__internal/ui/m_autocomplete.ts @@ -142,14 +142,18 @@ class Autocomplete extends DropDownList { } _dataSourceOptions() { + const { maxItemCount } = this.option(); return { paginate: true, - pageSize: this.option('maxItemCount'), + pageSize: maxItemCount, }; } _searchDataSource(searchValue): void { - this._dataSource.pageSize(this.option('maxItemCount')); + const { maxItemCount } = this.option(); + if (maxItemCount) { + this._dataSource.pageSize(maxItemCount); + } super._searchDataSource(searchValue); this._clearFocusedItem(); } diff --git a/packages/devextreme/js/__internal/ui/m_select_box.ts b/packages/devextreme/js/__internal/ui/m_select_box.ts index c876a2d63fb8..d3c35d847391 100644 --- a/packages/devextreme/js/__internal/ui/m_select_box.ts +++ b/packages/devextreme/js/__internal/ui/m_select_box.ts @@ -398,8 +398,9 @@ class SelectBox< } } - _isCustomValueAllowed() { - return this.option('acceptCustomValue') || super._isCustomValueAllowed(); + _isCustomValueAllowed(): boolean { + const { acceptCustomValue } = this.option(); + return Boolean(acceptCustomValue) || super._isCustomValueAllowed(); } _displayValue(item) { @@ -471,6 +472,7 @@ class SelectBox< } _getActualSearchValue() { + // @ts-expect-error fix argument type in m_data_controller.ts return this._dataController.searchValue(); } @@ -785,11 +787,12 @@ class SelectBox< const that = this; const deferred = Deferred(); - super._loadItem(value, cache) + (super._loadItem(value, cache) as DeferredObj) .done((item) => { deferred.resolve(item); }) .fail((args) => { + // @ts-expect-error add shouldSkipCallback to args if (args?.shouldSkipCallback) { return; } @@ -900,7 +903,6 @@ class SelectBox< return undefined; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars _searchHandler(e?): void { if (this._preventFiltering) { delete this._preventFiltering; @@ -911,7 +913,7 @@ class SelectBox< this._wasSearch(true); } - super._searchHandler(arguments); + super._searchHandler(e); } _dataSourceFiltered(searchValue?): void { @@ -943,7 +945,8 @@ class SelectBox< return; } - const item = this._list && this._getPlainItems(this._list.option('items'))[0]; + const { items } = this._list?.option() ?? {}; + const item = this._list && this._getPlainItems(items)[0]; if (!item) { return; diff --git a/packages/devextreme/js/__internal/ui/m_tag_box.ts b/packages/devextreme/js/__internal/ui/m_tag_box.ts index d96f647bfdb8..ce929c7f33fb 100644 --- a/packages/devextreme/js/__internal/ui/m_tag_box.ts +++ b/packages/devextreme/js/__internal/ui/m_tag_box.ts @@ -842,6 +842,7 @@ class TagBox< } _getFilter(creator) { + // @ts-expect-error fix argument type in m_data_controller.ts const dataSourceFilter = this._dataController.filter(); const filterExpr = creator.getCombinedFilter(this.option('valueExpr'), dataSourceFilter); const filterQueryLength = encodeURI(JSON.stringify(filterExpr)).length; @@ -1614,6 +1615,7 @@ class TagBox< const filter = this._dataSourceFilterExpr(); if (this._userFilter === undefined) { + // @ts-expect-error this._userFilter = dataController.filter() || null; } // @ts-expect-error ts-error @@ -1672,6 +1674,7 @@ class TagBox< const currentValue = value || []; const existedItems = listValues.length ? getIntersection(currentValue, listValues) : []; const newItems = existedItems.length + // @ts-expect-error fix on core/m_array level ? removeDuplicates(listValues, currentValue) : listValues; @@ -1683,8 +1686,10 @@ class TagBox< return []; } + const { selectedItems } = this._list.option(); + return this - ._getPlainItems(this._list.option('selectedItems')) + ._getPlainItems(selectedItems) // @ts-expect-error _valueGetter is injected by DataExpressionMixin .map((item) => this._valueGetter(item)); } From e41cba1fd6b3acf83590fea79dd188e48351a34e Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 3 Jun 2026 13:12:58 +0300 Subject: [PATCH 3/4] DropDownButton: Improve types --- .../ui/drop_down_editor/m_drop_down_button.ts | 75 +++++++++++++------ .../ui/drop_down_editor/m_drop_down_editor.ts | 2 +- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts index 164197a1fe36..49350401d010 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts @@ -3,20 +3,34 @@ import messageLocalization from '@js/common/core/localization/message'; import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import { extend } from '@js/core/utils/extend'; +import type { DxEvent } from '@js/events'; import type { Properties as ButtonProperties } from '@js/ui/button'; import Button from '@js/ui/button'; +import type TextEditorBase from '../text_box/text_editor.base'; import TextEditorButton from '../text_box/texteditor_button_collection/button'; +import type DropDownEditor from './m_drop_down_editor'; +import type { DropDownEditorProperties } from './m_drop_down_editor'; const DROP_DOWN_EDITOR_BUTTON_CLASS = 'dx-dropdowneditor-button'; const DROP_DOWN_EDITOR_BUTTON_VISIBLE = 'dx-dropdowneditor-button-visible'; +const STATE_FOCUSED_CLASS = 'dx-state-focused'; +const BUTTON_CLASS = 'dx-button'; +const BUTTON_MODE_CONTAINED_CLASS = 'dx-button-mode-contained'; const BUTTON_MESSAGE = 'dxDropDownEditor-selectLabel'; +type DropDownButtonOptions = Pick & { useInkRipple: boolean }; + export default class DropDownButton extends TextEditorButton { - currentTemplate: any; + // @ts-expect-error narrow type to enable DropDownEditor-specific options + declare editor: DropDownEditor | null; + + declare instance: Button | null; + + currentTemplate: DropDownEditorProperties['dropDownButtonTemplate'] | null; - constructor(name, editor, options) { + constructor(name: string, editor: TextEditorBase, options: ButtonProperties) { super(name, editor, options); this.currentTemplate = null; @@ -32,7 +46,6 @@ export default class DropDownButton extends TextEditorButton { return; } - // @ts-expect-error openOnFieldClick should be typed const { openOnFieldClick } = this.editor?.option() ?? {}; if (!openOnFieldClick) { @@ -41,8 +54,8 @@ export default class DropDownButton extends TextEditorButton { } }); - eventsEngine.on(instance.$element(), 'mousedown', (e) => { - if (this.editor?.$element().is('.dx-state-focused')) { + eventsEngine.on(instance.$element(), 'mousedown', (e: DxEvent) => { + if (this.editor?.$element().is(`.${STATE_FOCUSED_CLASS}`)) { e.preventDefault(); } }); @@ -85,56 +98,69 @@ export default class DropDownButton extends TextEditorButton { }; } - _getOptions() { + _getOptions(): DropDownButtonOptions { const { editor } = this; - const visible = this._isVisible(); - const isReadOnly = editor?.option('readOnly'); + const { readOnly } = editor?.option() ?? {}; const options = { focusStateEnabled: false, hoverStateEnabled: false, activeStateEnabled: false, useInkRipple: false, - disabled: isReadOnly, + disabled: readOnly, visible, }; this._addTemplate(options); + return options; } _isVisible(): boolean { const { editor } = this; - // @ts-expect-error - return super._isVisible() && editor?.option('showDropDownButton'); + const { showDropDownButton } = editor?.option() ?? {}; + + return super._isVisible() && !!showDropDownButton; } // TODO: get rid of it - _legacyRender($editor, $element, isVisible) { - $editor.toggleClass(DROP_DOWN_EDITOR_BUTTON_VISIBLE, isVisible); + // eslint-disable-next-line class-methods-use-this + _legacyRender( + $editor?: dxElementWrapper, + $element?: dxElementWrapper, + isVisible?: boolean, + ): void { + $editor?.toggleClass(DROP_DOWN_EDITOR_BUTTON_VISIBLE, isVisible); if ($element) { $element - .removeClass('dx-button') - .removeClass('dx-button-mode-contained') + .removeClass(BUTTON_CLASS) + .removeClass(BUTTON_MODE_CONTAINED_CLASS) .addClass(DROP_DOWN_EDITOR_BUTTON_CLASS); } } - _isSameTemplate() { - return this.editor?.option('dropDownButtonTemplate') === this.currentTemplate; + _isSameTemplate(): boolean { + const { editor } = this; + const { dropDownButtonTemplate } = editor?.option() ?? {}; + + return dropDownButtonTemplate === this.currentTemplate; } - _addTemplate(options): void { - if (!this._isSameTemplate()) { - options.template = this.editor?._getTemplateByOption('dropDownButtonTemplate'); - this.currentTemplate = this.editor?.option('dropDownButtonTemplate'); + _addTemplate(options: DropDownButtonOptions): void { + if (this._isSameTemplate()) { + return; } + + const { editor } = this; + const { dropDownButtonTemplate } = editor?.option() ?? {}; + + options.template = this.editor?._getTemplateByOption('dropDownButtonTemplate'); + this.currentTemplate = dropDownButtonTemplate; } - // @ts-expect-error - update(): void { + update(): boolean { const shouldUpdate = super.update(); if (shouldUpdate) { @@ -143,9 +169,10 @@ export default class DropDownButton extends TextEditorButton { const $editor = editor?.$element(); const options = this._getOptions(); - // @ts-expect-error instance?.option(options); this._legacyRender($editor, (instance as Button)?.$element(), options.visible); } + + return false; } } diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts index 1d6c6a772724..07007901474c 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_editor.ts @@ -681,7 +681,7 @@ class DropDownEditor< if (isIOs) { this._detachFocusOutEvents(); // @ts-expect-error NAME property is missing - eventsEngine.on(this._inputWrapper(), addNamespace('focusout', this.NAME), (event: FocusEvent) => { + eventsEngine.on(this._inputWrapper(), addNamespace('focusout', this.NAME), (event: DxEvent) => { const newTarget = event.relatedTarget; const { opened } = this.option(); if (newTarget && opened) { From 0760616c3a1a70f005c9639dee0f72f6e52e114a Mon Sep 17 00:00:00 2001 From: Ruslan Farkhutdinov Date: Wed, 3 Jun 2026 13:31:21 +0300 Subject: [PATCH 4/4] DropDownEditors: Fix notes --- .../__internal/ui/drop_down_editor/m_drop_down_button.ts | 7 +++---- .../js/__internal/ui/drop_down_editor/m_drop_down_list.ts | 4 ++-- packages/devextreme/js/__internal/ui/m_autocomplete.ts | 3 ++- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts index 49350401d010..7c5a32078884 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_button.ts @@ -160,7 +160,8 @@ export default class DropDownButton extends TextEditorButton { this.currentTemplate = dropDownButtonTemplate; } - update(): boolean { + // @ts-expect-error inconsistent return type, fix in TextEditorButton + update(): void { const shouldUpdate = super.update(); if (shouldUpdate) { @@ -170,9 +171,7 @@ export default class DropDownButton extends TextEditorButton { const options = this._getOptions(); instance?.option(options); - this._legacyRender($editor, (instance as Button)?.$element(), options.visible); + this._legacyRender($editor, instance?.$element(), options.visible); } - - return false; } } diff --git a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts index e03432843844..080378951fdf 100644 --- a/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts +++ b/packages/devextreme/js/__internal/ui/drop_down_editor/m_drop_down_list.ts @@ -461,7 +461,7 @@ class DropDownList< _isSelectedValue(value: unknown, cache?: ItemCache): boolean { const { value: optionValue } = this.option(); - // @ts-expect-error refacotor DataExpressionMixin + // @ts-expect-error refactor DataExpressionMixin return this._isValueEquals(value, optionValue) as boolean; } @@ -927,7 +927,7 @@ class DropDownList< if (this._dataController.pageIndex() === 0) { this.option().items = newItems; } else { - this.option().items = this.option().items?.concat(newItems); + this.option().items = (this.option().items ?? []).concat(newItems); } } diff --git a/packages/devextreme/js/__internal/ui/m_autocomplete.ts b/packages/devextreme/js/__internal/ui/m_autocomplete.ts index d7ac5d3b7db8..39da053ba0e7 100644 --- a/packages/devextreme/js/__internal/ui/m_autocomplete.ts +++ b/packages/devextreme/js/__internal/ui/m_autocomplete.ts @@ -4,6 +4,7 @@ import $ from '@js/core/renderer'; import { Deferred } from '@js/core/utils/deferred'; import { extend } from '@js/core/utils/extend'; import type { Properties } from '@js/ui/autocomplete'; +import { isDefined } from '@ts/core/utils/m_type'; import type { OptionChanged } from '@ts/core/widget/types'; import DropDownList from '@ts/ui/drop_down_editor/m_drop_down_list'; @@ -151,7 +152,7 @@ class Autocomplete extends DropDownList { _searchDataSource(searchValue): void { const { maxItemCount } = this.option(); - if (maxItemCount) { + if (isDefined(maxItemCount)) { this._dataSource.pageSize(maxItemCount); } super._searchDataSource(searchValue);