diff --git a/packages/devextreme/js/__internal/core/m_events_strategy.ts b/packages/devextreme/js/__internal/core/m_events_strategy.ts index b5468f71ad6f..8a5e59ab5192 100644 --- a/packages/devextreme/js/__internal/core/m_events_strategy.ts +++ b/packages/devextreme/js/__internal/core/m_events_strategy.ts @@ -70,5 +70,7 @@ export class EventsStrategy { each(this._events, (eventName, event) => { event.empty(); }); + + this._owner = null; } } diff --git a/packages/devextreme/js/__internal/core/widget/widget.ts b/packages/devextreme/js/__internal/core/widget/widget.ts index de0ec9a78878..c28b69b071b3 100644 --- a/packages/devextreme/js/__internal/core/widget/widget.ts +++ b/packages/devextreme/js/__internal/core/widget/widget.ts @@ -336,7 +336,7 @@ class Widget< // eslint-disable-next-line @typescript-eslint/prefer-optional-chain const $focusTarget = $element && $element.length ? $element : this._focusTarget(); - $focusTarget.toggleClass(FOCUSED_STATE_CLASS, isFocused); + $focusTarget?.toggleClass(FOCUSED_STATE_CLASS, isFocused); } _hasFocusClass(element?: dxElementWrapper): boolean { @@ -413,7 +413,7 @@ class Widget< _cleanFocusState(): void { const $element = this._focusTarget(); - $element.removeAttr('tabIndex'); + $element?.removeAttr('tabIndex'); this._toggleFocusClass(false); this._detachFocusEvents(); this._detachKeyboardEvents(); diff --git a/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts b/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts index f695debd79d1..a31bdcf8fcf3 100644 --- a/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts +++ b/packages/devextreme/js/__internal/ui/collection/collection_widget.base.ts @@ -558,7 +558,7 @@ class CollectionWidget< return shouldSkipRefreshId; } - _refreshActiveDescendant($target?: dxElementWrapper): void { + _refreshActiveDescendant($target?: dxElementWrapper | null): void { const { focusedElement } = this.option(); if (isDefined(focusedElement)) { diff --git a/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts b/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts index d3c682be812b..f3e6e00768d4 100644 --- a/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts +++ b/packages/devextreme/js/__internal/ui/color_box/m_color_box.ts @@ -148,7 +148,13 @@ class ColorBox extends DropDownEditor { _createColorView(): void { this._popup.$overlayContent().addClass(COLOR_BOX_OVERLAY_CLASS); - const $colorView = $('
').appendTo(this._popup.$content()); + const $content = this._popup.$content(); + + if (!$content) { + return; + } + + const $colorView = $('
').appendTo($content); this._colorView = this._createComponent($colorView, ColorView, this._colorViewConfig()); } diff --git a/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts b/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts index 1bafb5786d26..c1d5058d7975 100644 --- a/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts +++ b/packages/devextreme/js/__internal/ui/context_menu/m_context_menu.ts @@ -181,7 +181,7 @@ class ContextMenu extends MenuBase { _itemContainer(): dxElementWrapper { // @ts-expect-error - return this._overlay ? this._overlay.$content() : $(); + return this._overlay?.$content() ?? $(); } _eventBindingTarget(): dxElementWrapper { @@ -425,6 +425,11 @@ class ContextMenu extends MenuBase { // @ts-expect-error const $overlayContent = this._overlay.$content(); + + if (!$overlayContent) { + return; + } + $overlayContent.addClass(DX_CONTEXT_MENU_CLASS); this._addCustomCssClass($overlayContent); @@ -669,7 +674,7 @@ class ContextMenu extends MenuBase { _getItemsContainers(): dxElementWrapper { // @ts-expect-error - return this._overlay.$content().find(`.${DX_MENU_ITEMS_CONTAINER_CLASS}`); + return this._overlay?.$content()?.find(`.${DX_MENU_ITEMS_CONTAINER_CLASS}`) ?? $(); } _searchActiveItem(target): dxElementWrapper { @@ -977,7 +982,7 @@ class ContextMenu extends MenuBase { _hideAllShownSubmenus(): void { const shownSubmenus = extend([], this._shownSubmenus); // @ts-expect-error - const $expandedItems = this._overlay.$content().find(`.${DX_MENU_ITEM_EXPANDED_CLASS}`); + const $expandedItems = this._overlay?.$content()?.find(`.${DX_MENU_ITEM_EXPANDED_CLASS}`) ?? $(); $expandedItems.removeClass(DX_MENU_ITEM_EXPANDED_CLASS); @@ -1051,8 +1056,7 @@ class ContextMenu extends MenuBase { if (position) { if (!this._overlay) { this._renderContextMenuOverlay(); - // @ts-expect-error - this._overlay.$content().addClass(this._widgetClass()); + (this._overlay as unknown as Overlay).$content()?.addClass(this._widgetClass()); this._renderFocusState(); this._attachHoverEvents(); this._attachClickEvent(); diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts index 6c194143a0ad..89d1b798a577 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.base.ts @@ -377,7 +377,7 @@ class DateBox extends DropDownEditor { _renderPopup(): void { super._renderPopup(); - this._popup?.$wrapper().addClass(DATEBOX_WRAPPER_CLASS); + this._popup?.$wrapper()?.addClass(DATEBOX_WRAPPER_CLASS); this._renderPopupWrapper(); } @@ -411,7 +411,7 @@ class DateBox extends DropDownEditor { const { type } = this.option(); this._popup.$wrapper() - .addClass(`${DATEBOX_WRAPPER_CLASS}-${type}`) + ?.addClass(`${DATEBOX_WRAPPER_CLASS}-${type}`) .addClass(`${DATEBOX_WRAPPER_CLASS}-${this._pickerType}`) .addClass(DROPDOWNEDITOR_OVERLAY_CLASS); } diff --git a/packages/devextreme/js/__internal/ui/drawer/m_drawer.rendering.strategy.overlap.ts b/packages/devextreme/js/__internal/ui/drawer/m_drawer.rendering.strategy.overlap.ts index f243110da4b6..bfbdee118b50 100644 --- a/packages/devextreme/js/__internal/ui/drawer/m_drawer.rendering.strategy.overlap.ts +++ b/packages/devextreme/js/__internal/ui/drawer/m_drawer.rendering.strategy.overlap.ts @@ -161,19 +161,22 @@ class OverlapStrategy extends DrawerStrategy { } else if (revealMode === 'expand') { // @ts-expect-error this._initialPosition = drawer.isHorizontalDirection() ? { left: 0 } : { top: 0 }; - // @ts-expect-error - move($panelOverlayContent, this._initialPosition); - animation.size({ - complete: () => { - whenAnimationCompleted.resolve(); - }, - duration: drawer.option('animationDuration'), - direction: targetPanelPosition, - $element: $panelOverlayContent, - size: panelSize, - marginTop, - }); + if ($panelOverlayContent) { + // @ts-expect-error + move($panelOverlayContent, this._initialPosition); + + animation.size({ + complete: () => { + whenAnimationCompleted?.resolve(); + }, + duration: drawer.option('animationDuration'), + direction: targetPanelPosition, + $element: $panelOverlayContent, + size: panelSize, + marginTop, + }); + } } } else if (revealMode === 'slide') { // @ts-expect-error @@ -183,8 +186,10 @@ class OverlapStrategy extends DrawerStrategy { } else if (revealMode === 'expand') { // @ts-expect-error this._initialPosition = drawer.isHorizontalDirection() ? { left: 0 } : { top: 0 }; - // @ts-expect-error - move($panelOverlayContent, this._initialPosition); + if ($panelOverlayContent) { + // @ts-expect-error + move($panelOverlayContent, this._initialPosition); + } // @ts-expect-error if (drawer.isHorizontalDirection()) { $($panelOverlayContent).css('width', panelSize); 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 e7ecc8f8f90b..13edf75c50c1 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 @@ -21,38 +21,59 @@ export default class DropDownButton extends TextEditorButton { this.currentTemplate = null; } - _attachEvents(instance): void { - const { editor } = this; - + _attachEvents(instance: Button): void { instance.option('onClick', (e) => { - // @ts-expect-error - if (editor._shouldCallOpenHandler?.()) { - // @ts-expect-error - editor._openHandler(e); + // @ts-expect-error _shouldCallOpenHandler should be typed + if (this.editor?._shouldCallOpenHandler?.()) { + // @ts-expect-error _openHandler should be typed + this.editor?._openHandler(e); return; } - // @ts-expect-error - !editor.option('openOnFieldClick') && editor._openHandler(e); + + // @ts-expect-error openOnFieldClick should be typed + const { openOnFieldClick } = this.editor?.option() ?? {}; + + if (!openOnFieldClick) { + // @ts-expect-error _openHandler should be typed + this.editor?._openHandler(e); + } }); eventsEngine.on(instance.$element(), 'mousedown', (e) => { - if (editor.$element().is('.dx-state-focused')) { + if (this.editor?.$element()?.is('.dx-state-focused')) { e.preventDefault(); } }); } _create(): { - $element: dxElementWrapper; instance: Button; - } { + $element: dxElementWrapper; + } | undefined { const { editor } = this; + + if (!editor) { + return undefined; + } + const $element = $('
'); const options = this._getOptions(); this._addToContainer($element); - const instance = editor._createComponent($element, Button, extend({}, options, { elementAttr: { 'aria-label': messageLocalization.format(BUTTON_MESSAGE) } })); + const instance = editor._createComponent( + $element, + Button, + extend( + {}, + options, + { + elementAttr: { + 'aria-label': messageLocalization.format(BUTTON_MESSAGE), + }, + }, + ), + ); this._legacyRender(editor.$element(), $element, options.visible); @@ -65,7 +86,7 @@ export default class DropDownButton extends TextEditorButton { _getOptions() { const { editor } = this; const visible = this._isVisible(); - const isReadOnly = editor.option('readOnly'); + const isReadOnly = editor?.option('readOnly'); const options = { focusStateEnabled: false, hoverStateEnabled: false, @@ -82,7 +103,7 @@ export default class DropDownButton extends TextEditorButton { _isVisible(): boolean { const { editor } = this; // @ts-expect-error - return super._isVisible() && editor.option('showDropDownButton'); + return super._isVisible() && editor?.option('showDropDownButton'); } // TODO: get rid of it @@ -98,13 +119,13 @@ export default class DropDownButton extends TextEditorButton { } _isSameTemplate() { - return this.editor.option('dropDownButtonTemplate') === this.currentTemplate; + return this.editor?.option('dropDownButtonTemplate') === this.currentTemplate; } _addTemplate(options): void { if (!this._isSameTemplate()) { - options.template = this.editor._getTemplateByOption('dropDownButtonTemplate'); - this.currentTemplate = this.editor.option('dropDownButtonTemplate'); + options.template = this.editor?._getTemplateByOption('dropDownButtonTemplate'); + this.currentTemplate = this.editor?.option('dropDownButtonTemplate'); } } @@ -114,11 +135,14 @@ export default class DropDownButton extends TextEditorButton { if (shouldUpdate) { const { editor, instance } = this; - const $editor = editor.$element(); + + const $editor = editor?.$element(); const options = this._getOptions(); + // @ts-expect-error instance?.option(options); - this._legacyRender($editor, instance?.$element(), options.visible); + + this._legacyRender($editor, (instance as Button)?.$element(), options.visible); } } } 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 869b7afa2533..0179b16e4f7d 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 @@ -648,8 +648,8 @@ class DropDownEditor< }); this._attachPopupKeyHandler(); - this._contentReadyHandler(); + this._setPopupContentId(this._popup.$content()); this._bindInnerWidgetOptions(this._popup, 'dropDownOptions'); @@ -690,7 +690,7 @@ class DropDownEditor< this.close(); } - _setPopupContentId($popupContent: dxElementWrapper): void { + _setPopupContentId($popupContent?: dxElementWrapper | null): void { this._popupContentId = `dx-${new Guid()}`; this.setAria('id', this._popupContentId, $popupContent); } @@ -848,14 +848,13 @@ class DropDownEditor< } _clean(): void { - delete this._openOnFieldClickAction; - delete this._$templateWrapper; + this._$popup?.remove(); + + this._openOnFieldClickAction = undefined; + this._$templateWrapper = undefined; + this._popup = undefined; + this._$popup = undefined; - if (this._$popup) { - this._$popup.remove(); - delete this._$popup; - delete this._popup; - } super._clean(); } diff --git a/packages/devextreme/js/__internal/ui/m_action_sheet.ts b/packages/devextreme/js/__internal/ui/m_action_sheet.ts index 2e2f5d990132..d6fabd8e9370 100644 --- a/packages/devextreme/js/__internal/ui/m_action_sheet.ts +++ b/packages/devextreme/js/__internal/ui/m_action_sheet.ts @@ -137,7 +137,7 @@ class ActionSheet extends CollectionWidget { _renderPopupTitle(): void { this._mapPopupOption('showTitle'); - this._popup?.$wrapper().toggleClass(ACTION_SHEET_WITHOUT_TITLE_CLASS, !this.option('showTitle')); + this._popup?.$wrapper()?.toggleClass(ACTION_SHEET_WITHOUT_TITLE_CLASS, !this.option('showTitle')); } _clean() { @@ -169,9 +169,8 @@ class ActionSheet extends CollectionWidget { target: this.option('target'), })); - this._popup.$overlayContent().attr('role', 'dialog'); - - this._popup.$wrapper().addClass(ACTION_SHEET_POPOVER_WRAPPER_CLASS); + this._popup.$overlayContent()?.attr('role', 'dialog'); + this._popup.$wrapper()?.addClass(ACTION_SHEET_POPOVER_WRAPPER_CLASS); } _createPopup(): void { @@ -225,11 +224,11 @@ class ActionSheet extends CollectionWidget { }, })); - this._popup.$wrapper().addClass(ACTION_SHEET_POPUP_WRAPPER_CLASS); + this._popup.$wrapper()?.addClass(ACTION_SHEET_POPUP_WRAPPER_CLASS); } _popupContentReadyAction(): void { - this._popup.$content().append(this._$itemContainer); + this._popup.$content()?.append(this._$itemContainer); this._attachClickEvent(); this._attachHoldEvent(); @@ -250,10 +249,19 @@ class ActionSheet extends CollectionWidget { if (this.option('showCancelButton')) { const cancelClickAction = this._createActionByOption('onCancelClick') || noop; + + const $content = this._popup?.$content(); + + if (!$content) { + return; + } + const that = this; - this._$cancelButton = $('
').addClass(ACTION_SHEET_CANCEL_BUTTON_CLASS) - .appendTo(this._popup?.$content()); + this._$cancelButton = $('
') + .addClass(ACTION_SHEET_CANCEL_BUTTON_CLASS) + .appendTo($content); + this._createComponent(this._$cancelButton, Button, { disabled: false, stylingMode: ACTION_SHEET_BUTTON_DEFAULT_STYLING_MODE, diff --git a/packages/devextreme/js/__internal/ui/m_dialog.ts b/packages/devextreme/js/__internal/ui/m_dialog.ts index 5c4a850f5744..de0937b5b8c4 100644 --- a/packages/devextreme/js/__internal/ui/m_dialog.ts +++ b/packages/devextreme/js/__internal/ui/m_dialog.ts @@ -97,11 +97,11 @@ export const custom = function (options) { dragAndResizeArea: window, onContentReady(args) { args.component.$content() - .addClass(DX_DIALOG_CONTENT_CLASSNAME) + ?.addClass(DX_DIALOG_CONTENT_CLASSNAME) .append($message); if (messageId) { - args.component.$overlayContent().attr('aria-labelledby', messageId); + args.component.$overlayContent()?.attr('aria-labelledby', messageId); } }, onShowing(e) { @@ -179,14 +179,14 @@ export const custom = function (options) { popupInstance.option('toolbarItems', popupToolbarItems); - popupInstance.$wrapper().addClass(DX_DIALOG_WRAPPER_CLASSNAME); + popupInstance.$wrapper()?.addClass(DX_DIALOG_WRAPPER_CLASSNAME); if (options.position) { popupInstance.option('position', options.position); } popupInstance.$wrapper() - .addClass(DX_DIALOG_ROOT_CLASSNAME); + ?.addClass(DX_DIALOG_ROOT_CLASSNAME); function show() { if (devices.real().deviceType === 'phone') { diff --git a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts index 0c32ee0749d7..77c31ef35f10 100644 --- a/packages/devextreme/js/__internal/ui/m_drop_down_box.ts +++ b/packages/devextreme/js/__internal/ui/m_drop_down_box.ts @@ -226,8 +226,13 @@ class DropDownBox< if (!(contentTemplate && this.option('contentTemplate'))) { return; } - // @ts-expect-error ts-error - const $popupContent = this._popup.$content(); + + const $popupContent = this._popup?.$content(); + + if (!$popupContent) { + return; + } + const templateData = { value: this._fieldRenderData(), component: this, diff --git a/packages/devextreme/js/__internal/ui/m_drop_down_button.ts b/packages/devextreme/js/__internal/ui/m_drop_down_button.ts index 243cf5428eeb..ea41e6d098d3 100644 --- a/packages/devextreme/js/__internal/ui/m_drop_down_button.ts +++ b/packages/devextreme/js/__internal/ui/m_drop_down_button.ts @@ -370,13 +370,13 @@ class DropDownButton extends Widget { const $content = this._popup!.$content(); const template = this._getTemplateByOption('dropDownContentTemplate'); - $content.empty(); + $content?.empty(); this._popupContentId = `dx-${new Guid()}`; this.setAria('id', this._popupContentId, $content); const result = template.render({ - container: getPublicElement($content), + container: $content ? getPublicElement($content) : undefined, model: this.option('items') || this._dataController.getDataSource(), }); @@ -480,8 +480,8 @@ class DropDownButton extends Widget { const $popup = $('
'); this.$element().append($popup); this._popup = this._createComponent($popup, Popup, this._popupOptions()); - this._popup.$content().addClass(DROP_DOWN_BUTTON_CONTENT); - this._popup.$wrapper().addClass(DROP_DOWN_BUTTON_POPUP_WRAPPER_CLASS); + this._popup.$content()?.addClass(DROP_DOWN_BUTTON_CONTENT); + this._popup.$wrapper()?.addClass(DROP_DOWN_BUTTON_POPUP_WRAPPER_CLASS); this._popup.$overlayContent().attr('aria-label', OVERLAY_CONTENT_LABEL); this._popup.on('hiding', this._popupHidingHandler.bind(this)); this._popup.on('showing', this._popupShowingHandler.bind(this)); diff --git a/packages/devextreme/js/__internal/ui/m_load_panel.ts b/packages/devextreme/js/__internal/ui/m_load_panel.ts index 7d2afa9885b5..6d6ddd91dbc3 100644 --- a/packages/devextreme/js/__internal/ui/m_load_panel.ts +++ b/packages/devextreme/js/__internal/ui/m_load_panel.ts @@ -100,13 +100,13 @@ class LoadPanel extends Overlay { super._render(); this.$element().addClass(LOADPANEL_CLASS); - this.$wrapper().addClass(LOADPANEL_WRAPPER_CLASS); + this.$wrapper()?.addClass(LOADPANEL_WRAPPER_CLASS); this._updateWrapperAria(); } _updateWrapperAria(): void { this.$wrapper() - .removeAttr('aria-label') + ?.removeAttr('aria-label') .removeAttr('role'); const showIndicator = this.option('showIndicator'); @@ -134,10 +134,16 @@ class LoadPanel extends Overlay { _renderContentImpl(): void { super._renderContentImpl(); - this.$content().addClass(LOADPANEL_CONTENT_CLASS); + const $content = this.$content(); + + if (!$content) { + return; + } + + this.$content()?.addClass(LOADPANEL_CONTENT_CLASS); this._$loadPanelContentWrapper = $('
').addClass(LOADPANEL_CONTENT_WRAPPER_CLASS); - this._$loadPanelContentWrapper.appendTo(this.$content()); + this._$loadPanelContentWrapper.appendTo($content); this._togglePaneVisible(); @@ -209,13 +215,13 @@ class LoadPanel extends Overlay { } _cleanPreviousContent(): void { - this.$content().find(`.${LOADPANEL_MESSAGE_CLASS}`).remove(); - this.$content().find(`.${LOADPANEL_INDICATOR_CLASS}`).remove(); + this.$content()?.find(`.${LOADPANEL_MESSAGE_CLASS}`).remove(); + this.$content()?.find(`.${LOADPANEL_INDICATOR_CLASS}`).remove(); delete this._$indicator; } _togglePaneVisible(): void { - this.$content().toggleClass(LOADPANEL_PANE_HIDDEN_CLASS, !this.option('showPane')); + this.$content()?.toggleClass(LOADPANEL_PANE_HIDDEN_CLASS, !this.option('showPane')); } _optionChanged(args: OptionChanged): void { diff --git a/packages/devextreme/js/__internal/ui/m_lookup.ts b/packages/devextreme/js/__internal/ui/m_lookup.ts index 3a1d33ef05cf..edd401a63a28 100644 --- a/packages/devextreme/js/__internal/ui/m_lookup.ts +++ b/packages/devextreme/js/__internal/ui/m_lookup.ts @@ -614,7 +614,7 @@ class Lookup extends DropDownList { } this._$popup!.addClass(LOOKUP_POPUP_CLASS); - this._popup!.$wrapper().addClass(LOOKUP_POPUP_WRAPPER_CLASS); + this._popup!.$wrapper()?.addClass(LOOKUP_POPUP_WRAPPER_CLASS); } _renderPopover() { @@ -653,7 +653,11 @@ class Lookup extends DropDownList { this._popup._$arrow.remove(); } - this._setPopupContentId(this._popup.$content()); + const $content = this._popup.$content(); + + if ($content) { + this._setPopupContentId($content); + } this._contentReadyHandler(); } @@ -937,9 +941,7 @@ class Lookup extends DropDownList { } _toggleSearchClass(isSearchEnabled) { - if (this._popup) { - this._popup.$wrapper().toggleClass(LOOKUP_POPUP_SEARCH_CLASS, isSearchEnabled); - } + this._popup?.$wrapper()?.toggleClass(LOOKUP_POPUP_SEARCH_CLASS, isSearchEnabled); } _setSearchPlaceholder(): void { diff --git a/packages/devextreme/js/__internal/ui/m_tooltip.ts b/packages/devextreme/js/__internal/ui/m_tooltip.ts index da061147e0c4..c8446cb5fe52 100644 --- a/packages/devextreme/js/__internal/ui/m_tooltip.ts +++ b/packages/devextreme/js/__internal/ui/m_tooltip.ts @@ -35,7 +35,7 @@ TProperties extends TooltipProperties = TooltipProperties, _render(): void { this.$element().addClass(TOOLTIP_CLASS); - this.$wrapper().addClass(TOOLTIP_WRAPPER_CLASS); + this.$wrapper()?.addClass(TOOLTIP_WRAPPER_CLASS); super._render(); } diff --git a/packages/devextreme/js/__internal/ui/m_validation_message.ts b/packages/devextreme/js/__internal/ui/m_validation_message.ts index 668f1f586da1..474aad075df8 100644 --- a/packages/devextreme/js/__internal/ui/m_validation_message.ts +++ b/packages/devextreme/js/__internal/ui/m_validation_message.ts @@ -81,10 +81,10 @@ class ValidationMessage extends Overlay { _toggleVisibilityClasses(visible): void { if (visible) { this.$element().addClass(INVALID_MESSAGE); - this.$wrapper().addClass(INVALID_MESSAGE); + this.$wrapper()?.addClass(INVALID_MESSAGE); } else { this.$element().removeClass(INVALID_MESSAGE); - this.$wrapper().removeClass(INVALID_MESSAGE); + this.$wrapper()?.removeClass(INVALID_MESSAGE); } } @@ -93,7 +93,7 @@ class ValidationMessage extends Overlay { const id = contentId ?? $(container).attr('aria-describedby'); this.$content() - .addClass(INVALID_MESSAGE_CONTENT) + ?.addClass(INVALID_MESSAGE_CONTENT) .attr('id', id); } @@ -118,7 +118,7 @@ class ValidationMessage extends Overlay { _toggleModeClass(): void { const { mode } = this.option(); this.$wrapper() - .toggleClass(INVALID_MESSAGE_AUTO, mode === 'auto') + ?.toggleClass(INVALID_MESSAGE_AUTO, mode === 'auto') .toggleClass(INVALID_MESSAGE_ALWAYS, mode === 'always'); } diff --git a/packages/devextreme/js/__internal/ui/menu/m_menu.ts b/packages/devextreme/js/__internal/ui/menu/m_menu.ts index c920f92285ed..2b2a1f7e1742 100644 --- a/packages/devextreme/js/__internal/ui/menu/m_menu.ts +++ b/packages/devextreme/js/__internal/ui/menu/m_menu.ts @@ -455,12 +455,12 @@ class Menu extends MenuBase { this._overlay = this._createComponent($('
'), Overlay, this._getAdaptiveOverlayOptions()); // @ts-expect-error this._overlay.$content() - .append(this._treeView.$element()) + ?.append(this._treeView.$element()) .addClass(DX_ADAPTIVE_MODE_CLASS) .addClass(this.option('cssClass')); // @ts-expect-error - this._overlay.$wrapper().addClass(DX_ADAPTIVE_MODE_OVERLAY_WRAPPER_CLASS); + this._overlay.$wrapper()?.addClass(DX_ADAPTIVE_MODE_OVERLAY_WRAPPER_CLASS); this._$adaptiveContainer.append($hamburger); this._$adaptiveContainer.append(this._overlay.$element()); @@ -743,7 +743,7 @@ class Menu extends MenuBase { _submenuMouseLeaveHandler($rootItem: dxElementWrapper, eventArgs): void { const target = $(eventArgs.relatedTarget).parents(`.${DX_CONTEXT_MENU_CLASS}`)[0]; - const contextMenu = this._getSubmenuByRootElement($rootItem).getOverlayContent()[0]; + const contextMenu = this._getSubmenuByRootElement($rootItem).getOverlayContent()?.[0]; if (this.option('hideSubmenuOnMouseLeave') && target !== contextMenu) { this._clearTimeouts(); @@ -758,8 +758,8 @@ class Menu extends MenuBase { // @ts-expect-error const isRootItemHovered = $(this._visibleSubmenu.$element().context).hasClass(DX_STATE_HOVER_CLASS); - const isSubmenuItemHovered = this._visibleSubmenu.getOverlayContent().find(`.${DX_STATE_HOVER_CLASS}`).length; - const hoveredElementFromSubMenu = this._visibleSubmenu.getOverlayContent().get(0).querySelector(':hover'); + const isSubmenuItemHovered = this._visibleSubmenu.getOverlayContent()?.find(`.${DX_STATE_HOVER_CLASS}`).length; + const hoveredElementFromSubMenu = this._visibleSubmenu.getOverlayContent()?.get(0).querySelector(':hover'); if (!hoveredElementFromSubMenu && !isSubmenuItemHovered && !isRootItemHovered) { this._visibleSubmenu.hide(); diff --git a/packages/devextreme/js/__internal/ui/menu/m_submenu.ts b/packages/devextreme/js/__internal/ui/menu/m_submenu.ts index ae2b55f22763..498ebafae69a 100644 --- a/packages/devextreme/js/__internal/ui/menu/m_submenu.ts +++ b/packages/devextreme/js/__internal/ui/menu/m_submenu.ts @@ -194,9 +194,9 @@ class Submenu extends ContextMenu { return this._overlay.option('visible'); } - getOverlayContent(): dxElementWrapper { - // @ts-expect-error - return this._overlay.$content(); + getOverlayContent(): dxElementWrapper | undefined | null { + // @ts-expect-error _overlay + return this._overlay?.$content(); } } diff --git a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts index bd404bb39160..0ff61c3bd032 100644 --- a/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts +++ b/packages/devextreme/js/__internal/ui/number_box/m_number_box.spins.ts @@ -13,17 +13,26 @@ const SPIN_CONTAINER_CLASS = 'dx-numberbox-spin-container'; const SPIN_TOUCH_FRIENDLY_CLASS = 'dx-numberbox-spin-touch-friendly'; export default class SpinButtons extends TextEditorButton { - _attachEvents(instance, $spinContainer) { + _attachEvents( + instance: dxElementWrapper, + $spinContainer: dxElementWrapper, + ): void { const { editor } = this; - // @ts-expect-error - const eventName = addNamespace(pointer.down, editor.NAME); + + if (!editor) { + return; + } + + const eventName = addNamespace(pointer.down, editor.NAME ?? ''); const $spinContainerChildren = $spinContainer.children(); + const pointerDownAction = editor._createAction( - // @ts-expect-error - (e) => editor._spinButtonsPointerDownHandler(e), + // @ts-expect-error Private API + (e) => { this.editor?._spinButtonsPointerDownHandler(e); }, ); eventsEngine.off($spinContainer, eventName); + eventsEngine.on( $spinContainer, eventName, @@ -32,14 +41,14 @@ export default class SpinButtons extends TextEditorButton { SpinButton.getInstance($spinContainerChildren.eq(0)).option( 'onChange', - // @ts-expect-error - (e) => editor._spinUpChangeHandler(e), + // @ts-expect-error Private API + (e) => { this.editor?._spinUpChangeHandler(e); }, ); SpinButton.getInstance($spinContainerChildren.eq(1)).option( 'onChange', - // @ts-expect-error - (e) => editor._spinDownChangeHandler(e), + // @ts-expect-error Private API + (e) => { this.editor?._spinDownChangeHandler(e); }, ); } @@ -48,6 +57,7 @@ export default class SpinButtons extends TextEditorButton { instance: dxElementWrapper; } { const { editor } = this; + const $spinContainer = $('
').addClass(SPIN_CONTAINER_CLASS); const $spinUp = $('
').appendTo($spinContainer); const $spinDown = $('
').appendTo($spinContainer); @@ -55,10 +65,10 @@ export default class SpinButtons extends TextEditorButton { this._addToContainer($spinContainer); - editor._createComponent($spinUp, SpinButton, extend({ direction: 'up' }, options)); - editor._createComponent($spinDown, SpinButton, extend({ direction: 'down' }, options)); + editor?._createComponent($spinUp, SpinButton, extend({ direction: 'up' }, options)); + editor?._createComponent($spinDown, SpinButton, extend({ direction: 'down' }, options)); - this._legacyRender(editor.$element(), this._isTouchFriendly(), options.visible); + this._legacyRender(editor?.$element(), this._isTouchFriendly(), options.visible); return { instance: $spinContainer, @@ -68,8 +78,9 @@ export default class SpinButtons extends TextEditorButton { _getOptions() { const { editor } = this; + const visible = this._isVisible(); - const disabled = editor.option('disabled'); + const disabled = editor?.option('disabled'); return { visible, @@ -81,13 +92,13 @@ export default class SpinButtons extends TextEditorButton { _isVisible() { const { editor } = this; - return super._isVisible() && editor.option('showSpinButtons'); + return super._isVisible() && editor?.option('showSpinButtons'); } _isTouchFriendly() { const { editor } = this; - return editor.option('showSpinButtons') && editor.option('useLargeSpinButtons'); + return editor?.option('showSpinButtons') && editor?.option('useLargeSpinButtons'); } // TODO: get rid of it @@ -102,7 +113,8 @@ export default class SpinButtons extends TextEditorButton { if (shouldUpdate) { const { editor, instance } = this; - const $editor = editor.$element(); + + const $editor = editor?.$element(); const isVisible = this._isVisible(); const isTouchFriendly = this._isTouchFriendly(); // @ts-expect-error diff --git a/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts b/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts index 4d27789c984e..39661e586bb1 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts +++ b/packages/devextreme/js/__internal/ui/overlay/m_overlay.ts @@ -108,9 +108,9 @@ interface OverlayProperties extends Properties { class Overlay< TProperties extends OverlayProperties = OverlayProperties, > extends Widget { - _$wrapper!: dxElementWrapper; + _$wrapper?: dxElementWrapper | null; - _$content!: dxElementWrapper; + _$content?: dxElementWrapper | null; _contentAlreadyRendered?: boolean; @@ -249,11 +249,12 @@ class Overlay< }); } - $wrapper(): dxElementWrapper { + $wrapper(): dxElementWrapper | null | undefined { return this._$wrapper; } - _eventBindingTarget(): dxElementWrapper { + // @ts-expect-error LSP + _eventBindingTarget(): dxElementWrapper | null | undefined { return this._$content; } @@ -318,7 +319,7 @@ class Overlay< _initInnerOverlayClass(): void { const { innerOverlay } = this.option(); - this._$content.toggleClass(INNER_OVERLAY_CLASS, innerOverlay); + this._$content?.toggleClass(INNER_OVERLAY_CLASS, innerOverlay); } _initHideTopOverlayHandler(handler: () => void): void { @@ -352,22 +353,33 @@ class Overlay< this._initPositionController(); } - _documentDownHandler(e): boolean | undefined { + _documentDownHandler(e): boolean { if (this._showAnimationProcessing) { this._stopAnimation(); } - const isAttachedTarget = $(window.document).is(e.target) || domUtils.contains(window.document, e.target); - const isInnerOverlay = $(e.target).closest(`.${INNER_OVERLAY_CLASS}`).length; - const outsideClick = isAttachedTarget && !isInnerOverlay && !(this._$content.is(e.target) - || domUtils.contains(this._$content.get(0), e.target)); - if (outsideClick && this._shouldHideOnOutsideClick(e)) { + const { target } = e; + const $target = $(target); + + const isTargetDocument = domUtils.contains(window.document, target); + const isAttachedTarget = $(window.document).is($target) || isTargetDocument; + const isInnerOverlay = $($target).closest(`.${INNER_OVERLAY_CLASS}`).length; + const isTargetContent = this._$content?.is($target); + const content = this._$content?.get(0); + const isTargetInContent = content ? domUtils.contains(content, target) : false; + + const isOutsideClick = isAttachedTarget + && !isInnerOverlay + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + && !(isTargetContent || isTargetInContent); + + if (isOutsideClick && this._shouldHideOnOutsideClick(e)) { this._outsideClickHandler(e); } const { propagateOutsideClick } = this.option(); - return propagateOutsideClick; + return Boolean(propagateOutsideClick); } _shouldHideOnOutsideClick(e): boolean | undefined { @@ -405,7 +417,7 @@ class Overlay< for (let i = overlayStack.length - 1; i >= 0; i--) { const tabbableElements = overlayStack[i]._findTabbableBounds(); - if (tabbableElements.first || tabbableElements.last) { + if (tabbableElements.$first || tabbableElements.$last) { // @ts-ignore return overlayStack[i] === this; } @@ -444,16 +456,21 @@ class Overlay< _renderWrapperAttributes(): void { const { wrapperAttr } = this.option(); - const attributes = extend({}, wrapperAttr); + + const attributes = { ...wrapperAttr }; const classNames = attributes.class; delete attributes.class; - // @ts-expect-error ts-error - this.$wrapper() - .attr(attributes) - // @ts-expect-error ts-error - .removeClass(this._customWrapperClass) - .addClass(classNames); + + const $wrapper = this.$wrapper(); + + $wrapper?.attr(attributes); + + if (this._customWrapperClass) { + $wrapper?.removeClass(this._customWrapperClass); + } + + $wrapper?.addClass(classNames); this._customWrapperClass = classNames; } @@ -549,8 +566,8 @@ class Overlay< this._toggleBodyScroll(enableBodyScroll); this._toggleVisibility(true); - this._$content.css('visibility', 'hidden'); - this._$content.toggleClass(INVISIBLE_STATE_CLASS, false); + this._$content?.css('visibility', 'hidden'); + this._$content?.toggleClass(INVISIBLE_STATE_CLASS, false); this._updateZIndexStackPosition(true); this._positionController.openingHandled(); this._renderContent(); @@ -560,8 +577,8 @@ class Overlay< const cancelShow = () => { this._toggleVisibility(false); - this._$content.css('visibility', ''); - this._$content.toggleClass(INVISIBLE_STATE_CLASS, true); + this._$content?.css('visibility', ''); + this._$content?.toggleClass(INVISIBLE_STATE_CLASS, true); this._isShowingActionCanceled = true; this._moveFromContainer(); this._toggleBodyScroll(true); @@ -570,7 +587,7 @@ class Overlay< }; const applyShow = () => { - this._$content.css('visibility', ''); + this._$content?.css('visibility', ''); this._renderVisibility(true); this._animateShowing(); }; @@ -610,7 +627,7 @@ class Overlay< this._animate( hideAnimation, (...args) => { - this._$content.css('pointerEvents', ''); + this._$content?.css('pointerEvents', ''); this._renderVisibility(false); completeHideAnimation.call(this, ...args); @@ -620,7 +637,7 @@ class Overlay< this._hidingDeferred.resolve(); }, (...args) => { - this._$content.css('pointerEvents', 'none'); + this._$content?.css('pointerEvents', 'none'); startHideAnimation.call(this, ...args); this._hideAnimationProcessing = true; }, @@ -668,7 +685,7 @@ class Overlay< _forceFocusLost(): void { const activeElement = domAdapter.getActiveElement(); - const shouldResetActiveElement = !!this._$content.find(activeElement).length; + const shouldResetActiveElement = !!this._$content?.find(activeElement).length; if (shouldResetActiveElement) { domUtils.resetActiveElement(); @@ -677,20 +694,29 @@ class Overlay< _animate(animation, completeCallback, startCallback): void { if (animation) { - startCallback = startCallback || animation.start || noop; - // @ts-expect-error ts-error - fx.animate(this._$content, extend({}, animation, { - start: startCallback, + const actualStartCallback = startCallback ?? animation.start ?? noop; + + const configuration = { + ...animation, + start: actualStartCallback, complete: completeCallback, - })); + }; + + if (this._$content) { + // @ts-expect-error this._$content + // eslint-disable-next-line @typescript-eslint/no-floating-promises + fx.animate(this._$content, configuration); + } } else { completeCallback(); } } _stopAnimation(): void { - // @ts-expect-error ts-error - fx.stop(this._$content, true); + if (this._$content) { + // @ts-expect-error this._$content + fx.stop(this._$content, true); + } } _renderVisibility(visible: boolean): void { @@ -715,7 +741,7 @@ class Overlay< triggerResizeEvent(this._$content); } else { this._toggleVisibility(visible); - this._$content.toggleClass(INVISIBLE_STATE_CLASS, !visible); + this._$content?.toggleClass(INVISIBLE_STATE_CLASS, !visible); this._updateZIndexStackPosition(visible); this._moveFromContainer(); } @@ -758,16 +784,16 @@ class Overlay< } _updateZIndex(): void { - this._$wrapper.css('zIndex', this._zIndex); - this._$content.css('zIndex', this._zIndex); + this._$wrapper?.css('zIndex', this._zIndex); + this._$content?.css('zIndex', this._zIndex); } _toggleShading(visible?: boolean): void { const { shading, shadingColor } = this.option(); - this._$wrapper.toggleClass(OVERLAY_SHADER_CLASS, visible && shading); + this._$wrapper?.toggleClass(OVERLAY_SHADER_CLASS, visible && shading); // @ts-expect-error ts-error - this._$wrapper.css('backgroundColor', shading ? shadingColor : ''); + this._$wrapper?.css('backgroundColor', shading ? shadingColor : ''); // @ts-expect-error ts-error this._toggleTabTerminator(visible && shading); } @@ -792,31 +818,36 @@ class Overlay< } } - _findTabbableBounds(): { first: dxElementWrapper | null; last: dxElementWrapper | null } { - const $elements = this._$wrapper.find('*'); - const elementsCount = $elements.length - 1; + _findTabbableBounds(): { + $first: dxElementWrapper | null; + $last: dxElementWrapper | null; + } { + const $elements = this._$wrapper?.find('*'); + const elementsCount = ($elements?.length ?? 0) - 1; - let first = null; - let last = null; + let $first: dxElementWrapper | null = null; + let $last: dxElementWrapper | null = null; for (let i = 0; i <= elementsCount; i += 1) { - // @ts-expect-error ts-error - if (!first && $elements.eq(i).is(tabbable)) { - // @ts-expect-error ts-error - first = $elements.eq(i); + const $currentElement = $elements?.eq(i) ?? null; + const $reverseElement = $elements?.eq(elementsCount - i) ?? null; + + // @ts-expect-error is should can get function as callback + if (!$first && $currentElement.is(tabbable)) { + $first = $currentElement; } - // @ts-expect-error ts-error - if (!last && $elements.eq(elementsCount - i).is(tabbable)) { - // @ts-expect-error ts-error - last = $elements.eq(elementsCount - i); + + // @ts-expect-error is should can get function as callback + if (!$last && $reverseElement.is(tabbable)) { + $last = $reverseElement; } - if (first && last) { + if ($first && $last) { break; } } - return { first, last }; + return { $first, $last }; } _tabKeyHandler(e: KeyboardEvent): void { @@ -824,10 +855,13 @@ class Overlay< return; } - const wrapper = this._$wrapper.get(0) as HTMLElement; + const wrapper = this._$wrapper?.get(0) as HTMLElement; const activeElement = domAdapter.getActiveElement(wrapper); - const { first: $firstTabbable, last: $lastTabbable } = this._findTabbableBounds(); + const { + $first: $firstTabbable, + $last: $lastTabbable, + } = this._findTabbableBounds(); const isTabOnLast = !e.shiftKey && activeElement === $lastTabbable?.get(0); const isShiftTabOnFirst = e.shiftKey && activeElement === $firstTabbable?.get(0); @@ -875,9 +909,9 @@ class Overlay< const hideOnScroll = this.option('hideOnParentScroll'); if (needSubscribe && hideOnScroll) { - let $parents = this._getHideOnParentScrollTarget().parents(); + let $parents = this._getHideOnParentScrollTarget()?.parents(); if (devices.real().deviceType === 'desktop') { - $parents = $parents.add(window); + $parents = $parents?.add(window); } eventsEngine.on($parents, scrollEvent, handler); this._parentsScrollSubscriptionInfo.prevTargets = $parents; @@ -896,7 +930,7 @@ class Overlay< } } - _getHideOnParentScrollTarget(): dxElementWrapper { + _getHideOnParentScrollTarget(): dxElementWrapper | null | undefined { // eslint-disable-next-line @typescript-eslint/naming-convention const { _hideOnParentScrollTarget } = this.option(); // @ts-expect-error ts-error @@ -917,8 +951,8 @@ class Overlay< } _appendContentToElement(): void { - if (!this._$content.parent().is(this.$element())) { - this._$content.appendTo(this.$element()); + if (!this._$content?.parent().is(this.$element())) { + this._$content?.appendTo(this.$element()); } } @@ -977,7 +1011,7 @@ class Overlay< const transclude = this._templateManager.anonymousTemplateName === contentTemplateOption; contentTemplate?.render({ - container: getPublicElement(this.$content()), + container: this.content(), noModel: true, transclude, onRendered: () => { @@ -1074,8 +1108,8 @@ class Overlay< } _moveFromContainer() { - this._$content.appendTo(this.$element()); - this._$wrapper.detach(); + this._$content?.appendTo(this.$element()); + this._$wrapper?.detach(); } _checkContainerExists() { @@ -1096,8 +1130,13 @@ class Overlay< _moveToContainer(): void { const $wrapperContainer = this._positionController.$container; - this._$wrapper.appendTo($wrapperContainer); - this._$content.appendTo(this._$wrapper); + if ($wrapperContainer !== undefined) { + this._$wrapper?.appendTo($wrapperContainer); + } + + if (this._$wrapper) { + this._$content?.appendTo(this._$wrapper); + } } // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -1123,9 +1162,11 @@ class Overlay< this._positionController.positionContent(); } - _isAllWindowCovered(): boolean | undefined { + _isAllWindowCovered(): boolean { const { shading } = this.option(); - return isWindow(this._positionController.$visualContainer.get(0)) && shading; + const element = this._positionController.$visualContainer?.get(0); + + return isWindow(element) && Boolean(shading); } _toggleSafariScrolling(): void { @@ -1162,7 +1203,7 @@ class Overlay< _renderWrapperDimensions(): void { const { $visualContainer } = this._positionController; const documentElement = domAdapter.getDocumentElement(); - const isVisualContainerWindow = isWindow($visualContainer.get(0)); + const isVisualContainerWindow = isWindow($visualContainer?.get(0)); const wrapperWidth = isVisualContainerWindow ? documentElement.clientWidth @@ -1171,16 +1212,16 @@ class Overlay< ? window.innerHeight : getOuterHeight($visualContainer); - this._$wrapper.css({ + this._$wrapper?.css({ width: wrapperWidth, height: wrapperHeight, }); } _renderDimensions(): void { - const content = this._$content.get(0); + const content = this._$content?.get(0); - this._$content.css({ + this._$content?.css({ minWidth: this._getOptionValue('minWidth', content), maxWidth: this._getOptionValue('maxWidth', content), minHeight: this._getOptionValue('minHeight', content), @@ -1190,7 +1231,8 @@ class Overlay< }); } - _focusTarget(): dxElementWrapper { + // @ts-expect-error LSP + _focusTarget(): dxElementWrapper | null | undefined { return this._$content; } @@ -1207,7 +1249,7 @@ class Overlay< const e = options.originalEvent; const $target = $(e.target); - if ($target.is(this._$content) || !this.option('ignoreChildEvents')) { + if ($target.is(this._$content ?? '') || !this.option('ignoreChildEvents')) { // @ts-expect-error ts-error super._keyboardHandler(...arguments); } @@ -1236,7 +1278,7 @@ class Overlay< _clean(): void { const { isRenovated } = this.option(); if (!this._contentAlreadyRendered && !isRenovated) { - this.$content().empty(); + this.$content()?.empty(); } this._renderVisibility(false); @@ -1244,27 +1286,37 @@ class Overlay< } _dispose(): void { - // @ts-expect-error - fx.stop(this._$content, false); + if (this._$content) { + // @ts-expect-error this._$content + fx.stop(this._$content, false); + } this._toggleViewPortSubscription(false); this._toggleSubscriptions(false); this._updateZIndexStackPosition(false); this._toggleTabTerminator(false); - this._actions = null; - this._parentsScrollSubscriptionInfo = null; - super._dispose(); this._toggleSafariScrolling(); - this.option('visible') && zIndexPool.remove(this._zIndex); - this._$wrapper.remove(); - this._$content.remove(); + + if (this._isVisible()) { + zIndexPool.remove(this._zIndex); + } + + this._positionController.clean(); + + this._$wrapper?.remove(); + this._$content?.remove(); + + this._actions = null; + this._parentsScrollSubscriptionInfo = null; + this._$wrapper = null; + this._$content = null; } _toggleRTLDirection(rtl) { - this._$content.toggleClass(RTL_DIRECTION_CLASS, rtl); + this._$content?.toggleClass(RTL_DIRECTION_CLASS, rtl); } _optionChanged(args: OptionChanged): void { @@ -1404,7 +1456,7 @@ class Overlay< return result.promise(); } - $content(): dxElementWrapper { + $content(): dxElementWrapper | null | undefined { return this._$content; } @@ -1417,7 +1469,7 @@ class Overlay< } content() { - return getPublicElement(this._$content); + return getPublicElement(this._$content as dxElementWrapper); } repaint(): void { diff --git a/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts b/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts index 5e617fee207c..f218257b2fe7 100644 --- a/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/overlay/m_overlay_position_controller.ts @@ -32,15 +32,15 @@ const OVERLAY_DEFAULT_BOUNDARY_OFFSET = { h: 0, v: 0 }; class OverlayPositionController { _props: any; - _$root: dxElementWrapper; + _$root?: dxElementWrapper; - _$content: dxElementWrapper; + _$content?: dxElementWrapper; - _$wrapper: dxElementWrapper; + _$wrapper?: dxElementWrapper; - _$markupContainer!: dxElementWrapper; + _$markupContainer?: dxElementWrapper; - _$visualContainer!: dxElementWrapper; + _$visualContainer?: dxElementWrapper; _shouldRenderContentInitialPosition: boolean; @@ -74,9 +74,7 @@ class OverlayPositionController { this._$root = $root; this._$content = $content; this._$wrapper = $wrapper; - // @ts-expect-error ts-error this._$markupContainer = undefined; - // @ts-expect-error ts-error this._$visualContainer = undefined; this._shouldRenderContentInitialPosition = true; @@ -89,13 +87,13 @@ class OverlayPositionController { this.updateVisualContainer(visualContainer); } - get $container() { + get $container(): dxElementWrapper | undefined { this.updateContainer(); // NOTE: swatch classes can be updated runtime return this._$markupContainer; } - get $visualContainer(): dxElementWrapper { + get $visualContainer(): dxElementWrapper | undefined { return this._$visualContainer; } @@ -156,7 +154,9 @@ class OverlayPositionController { if (this._shouldRenderContentInitialPosition) { this._renderContentInitialPosition(); } else { - move(this._$content, this._visualPosition); + if (this._$content) { + move(this._$content, this._visualPosition); + } this.detectVisualPositionChange(); } } @@ -168,30 +168,42 @@ class OverlayPositionController { } styleWrapperPosition(): void { - const useFixed = isWindow(this.$visualContainer.get(0)) || this._props._fixWrapperPosition; + const useFixed = isWindow(this.$visualContainer?.get(0)) || this._props._fixWrapperPosition; const positionStyle = useFixed ? 'fixed' : 'absolute'; - this._$wrapper.css('position', positionStyle); + this._$wrapper?.css('position', positionStyle); + } + + clean(): void { + this._$root = undefined; + this._$content = undefined; + this._$wrapper = undefined; + this._$markupContainer = undefined; + this._$visualContainer = undefined; } _updateVisualPositionValue(): void { this._previousVisualPosition = this._visualPosition; - this._visualPosition = locate(this._$content); + if (this._$content) { + this._visualPosition = locate(this._$content); + } } _renderContentInitialPosition(): void { this._renderBoundaryOffset(); - resetPosition(this._$content); + if (this._$content) { + resetPosition(this._$content); + } // @ts-expect-error ts-error - const wrapperOverflow = this._$wrapper.css('overflow'); - this._$wrapper.css('overflow', 'hidden'); + const wrapperOverflow = this._$wrapper?.css('overflow'); + this._$wrapper?.css('overflow', 'hidden'); if (!this._props._skipContentPositioning) { const resultPosition = positionUtils.setup(this._$content, this._position); this._initialPosition = resultPosition; } // @ts-expect-error ts-error - this._$wrapper.css('overflow', wrapperOverflow); + this._$wrapper?.css('overflow', wrapperOverflow); this.detectVisualPositionChange(); } @@ -218,7 +230,7 @@ class OverlayPositionController { _renderBoundaryOffset(): void { const boundaryOffset = this._position ?? { boundaryOffset: OVERLAY_DEFAULT_BOUNDARY_OFFSET }; - this._$content.css('margin', `${boundaryOffset.v}px ${boundaryOffset.h}px`); + this._$content?.css('margin', `${boundaryOffset.v}px ${boundaryOffset.h}px`); } _getVisualContainer(): dxElementWrapper { diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover.ts b/packages/devextreme/js/__internal/ui/popover/m_popover.ts index b61835803206..79c0f1b5c984 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover.ts @@ -148,7 +148,7 @@ TProperties extends PopoverProperties = PopoverProperties, this._timeouts = {}; this.$element().addClass(POPOVER_CLASS); - this.$wrapper().addClass(POPOVER_WRAPPER_CLASS); + this.$wrapper()?.addClass(POPOVER_WRAPPER_CLASS); const { toolbarItems } = this.option(); @@ -267,10 +267,11 @@ TProperties extends PopoverProperties = PopoverProperties, .prependTo(this.$overlayContent()); } - _documentDownHandler(e): boolean | undefined { + _documentDownHandler(e): boolean { if (this._isOutsideClick(e)) { return super._documentDownHandler(e); } + return true; } @@ -296,7 +297,7 @@ TProperties extends PopoverProperties = PopoverProperties, } _renderTitle(): void { - this.$wrapper().toggleClass(POPOVER_WITHOUT_TITLE_CLASS, !this.option('showTitle')); + this.$wrapper()?.toggleClass(POPOVER_WITHOUT_TITLE_CLASS, !this.option('showTitle')); super._renderTitle(); } @@ -376,13 +377,13 @@ TProperties extends PopoverProperties = PopoverProperties, _togglePositionClass(positionClass) { this.$wrapper() - .removeClass('dx-position-left dx-position-right dx-position-top dx-position-bottom') + ?.removeClass('dx-position-left dx-position-right dx-position-top dx-position-bottom') .addClass(positionClass); } _toggleFlippedClass(isFlippedHorizontal, isFlippedVertical) { this.$wrapper() - .toggleClass('dx-popover-flipped-horizontal', isFlippedHorizontal) + ?.toggleClass('dx-popover-flipped-horizontal', isFlippedHorizontal) .toggleClass('dx-popover-flipped-vertical', isFlippedVertical); } @@ -457,7 +458,7 @@ TProperties extends PopoverProperties = PopoverProperties, _renderWrapperDimensions() { if (this.option('shading')) { - this.$wrapper().css({ + this.$wrapper()?.css({ width: '100%', height: '100%', }); diff --git a/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts b/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts index 29aa705618c1..91fc66fc6c2b 100644 --- a/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/popover/m_popover_position_controller.ts @@ -53,7 +53,7 @@ class PopoverPositionController extends OverlayPositionController { positionWrapper(): void { if (this._props.shading) { - this._$wrapper.css({ top: 0, left: 0 }); + this._$wrapper?.css({ top: 0, left: 0 }); } } @@ -85,7 +85,7 @@ class PopoverPositionController extends OverlayPositionController { } _getContentBorderWidth(side) { - const borderWidth = this._$content.css(borderWidthStyles[side]); + const borderWidth = this._$content?.css(borderWidthStyles[side]); // @ts-expect-error // eslint-disable-next-line radix diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup.ts b/packages/devextreme/js/__internal/ui/popup/m_popup.ts index 940f710b3824..18d8bbfab702 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup.ts @@ -143,11 +143,11 @@ class Popup< _bodyOverflowManager?: OverflowManager; - _$title?: dxElementWrapper; + _$title?: dxElementWrapper | null; - _$bottom?: dxElementWrapper; + _$bottom?: dxElementWrapper | null; - _$popupContent!: dxElementWrapper; + _$popupContent?: dxElementWrapper; _resizable!: Resizable; @@ -331,9 +331,9 @@ class Popup< this._updateResizeCallbackSkipCondition(); this.$element().addClass(POPUP_CLASS); - this.$wrapper().addClass(popupWrapperClasses); + this.$wrapper()?.addClass(popupWrapperClasses); this._$popupContent = this._$content - .wrapInner($('
').addClass(POPUP_CONTENT_CLASS)) + ?.wrapInner($('
').addClass(POPUP_CONTENT_CLASS)) .children().eq(0); this._toggleContentScrollClass(); @@ -401,7 +401,7 @@ class Popup< return; } - const contentElement = this._$content.get(0); + const contentElement = this._$content?.get(0); if (shouldObserve) { resizeObserverSingleton.observe(contentElement, ( entry: ResizeObserverEntry, @@ -453,10 +453,15 @@ class Popup< } if (showTitle || items.length > 0) { - if (this._$title) { - this._$title.remove(); + this._$title?.remove(); + + const $title = $('
').addClass(POPUP_TITLE_CLASS); + const $content = this.$content(); + + if ($content) { + $title.insertBefore($content); } - const $title = $('
').addClass(POPUP_TITLE_CLASS).insertBefore(this.$content()); + this._$title = this._renderTemplateByType('titleTemplate', items, $title).addClass(POPUP_TITLE_CLASS); this._renderDrag(); this._executeTitleRenderAction(this._$title); @@ -684,7 +689,14 @@ class Popup< if (items.length) { this._$bottom?.remove(); - const $bottom = $('
').addClass(POPUP_BOTTOM_CLASS).insertAfter(this.$content()); + + const $bottom = $('
').addClass(POPUP_BOTTOM_CLASS); + const $content = this.$content(); + + if ($content) { + $bottom.insertAfter($content); + } + this._$bottom = this._renderTemplateByType('bottomTemplate', items, $bottom, { compactMode: true }).addClass(POPUP_BOTTOM_CLASS); this._toggleClasses(); } else { @@ -696,7 +708,7 @@ class Popup< // @ts-expect-error ts-error super._toggleDisabledState(...arguments); - this.$content().toggleClass(DISABLED_STATE_CLASS, Boolean(value)); + this.$content()?.toggleClass(DISABLED_STATE_CLASS, Boolean(value)); } _toggleClasses(): void { @@ -706,10 +718,10 @@ class Popup< const className = `${POPUP_CLASS}-${alias}`; if (this._toolbarItemClasses.includes(className)) { - this.$wrapper().addClass(`${className}-visible`); + this.$wrapper()?.addClass(`${className}-visible`); this._$bottom?.addClass(className); } else { - this.$wrapper().removeClass(`${className}-visible`); + this.$wrapper()?.removeClass(`${className}-visible`); this._$bottom?.removeClass(className); } }); @@ -723,15 +735,15 @@ class Popup< zIndexPool.remove(this._zIndex); this._zIndex = zIndex; - this._$wrapper.css('zIndex', zIndex); - this._$content.css('zIndex', zIndex); + this._$wrapper?.css('zIndex', zIndex); + this._$content?.css('zIndex', zIndex); } } _toggleContentScrollClass(): void { const isNativeScrollingEnabled = !this.option('preventScrollEvents'); - this.$content().toggleClass(POPUP_CONTENT_SCROLLABLE_CLASS, isNativeScrollingEnabled); + this.$content()?.toggleClass(POPUP_CONTENT_SCROLLABLE_CLASS, isNativeScrollingEnabled); } _getPositionControllerConfig() { @@ -811,7 +823,7 @@ class Popup< const height = this._getOptionValue('height'); if (height === 'auto') { - this.$content().css({ + this.$content()?.css({ height: 'auto', maxHeight: 'none', }); @@ -829,7 +841,7 @@ class Popup< const config = { dragEnabled, handle: $dragTarget.get(0), - draggableElement: this._$content.get(0), + draggableElement: this._$content?.get(0), positionController: this._positionController, }; @@ -843,6 +855,10 @@ class Popup< } _renderResize(): void { + if (!this._$content) { + return; + } + this._resizable = this._createComponent(this._$content, Resizable, { handles: this.option('resizeEnabled') ? 'all' : 'none', onResizeEnd: (e) => { @@ -885,7 +901,7 @@ class Popup< const overlayContent = this.$overlayContent().get(0) as HTMLElement; const currentHeightStrategyClass = this._chooseHeightStrategy(overlayContent); - this.$content().css(this._getHeightCssStyles(currentHeightStrategyClass, overlayContent)); + this.$content()?.css(this._getHeightCssStyles(currentHeightStrategyClass, overlayContent)); this._setHeightClasses(this.$overlayContent(), currentHeightStrategyClass); } @@ -972,16 +988,15 @@ class Popup< header: getVisibleHeight(topToolbar?.get(0)), footer: getVisibleHeight(bottomToolbar?.get(0)), contentVerticalOffsets: getVerticalOffsets(this.$overlayContent().get(0), true), - popupVerticalOffsets: getVerticalOffsets(this.$content().get(0), true), - popupVerticalPaddings: getVerticalOffsets(this.$content().get(0), false), + popupVerticalOffsets: getVerticalOffsets(this.$content()?.get(0), true), + popupVerticalPaddings: getVerticalOffsets(this.$content()?.get(0), false), }; } - _isAllWindowCovered(): boolean | undefined { + _isAllWindowCovered(): boolean { const { fullScreen } = this.option(); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return super._isAllWindowCovered() || fullScreen; + return super._isAllWindowCovered() || Boolean(fullScreen); } _renderDimensions(): void { @@ -1015,6 +1030,10 @@ class Popup< super._dispose(); this._toggleBodyScroll(true); + + this._$title = undefined; + this._$bottom = undefined; + this._$popupContent = undefined; } _renderFullscreenWidthClass(): void { @@ -1162,29 +1181,37 @@ class Popup< } } - bottomToolbar(): dxElementWrapper | undefined { + bottomToolbar(): dxElementWrapper | undefined | null { return this._$bottom; } - topToolbar(): dxElementWrapper | undefined { + topToolbar(): dxElementWrapper | undefined | null { return this._$title; } - $content(): dxElementWrapper { + $content(): dxElementWrapper | null | undefined { return this._$popupContent; } - content() { - return getPublicElement(this.$content()); + content(): HTMLElement { + return getPublicElement(this.$content() as dxElementWrapper); } $overlayContent(): dxElementWrapper { - return this._$content; + return this._$content as dxElementWrapper; } getFocusableElements(): dxElementWrapper { - // @ts-expect-error ts-error - return this.$wrapper().find('[tabindex]').filter((index, item) => item.getAttribute('tabindex') >= 0); + const $wrapper = this.$wrapper(); + + if (!$wrapper) { + return $(); + } + + return $wrapper + .find('[tabindex]') + // @ts-expect-error ts-error + .filter((_, item) => item.getAttribute('tabindex') >= 0); } } diff --git a/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts b/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts index e9fa927585bd..c2eaad77d367 100644 --- a/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/popup/m_popup_position_controller.ts @@ -90,7 +90,9 @@ class PopupPositionController extends OverlayPositionController { positionContent(): void { if (this._props.fullScreen) { - move(this._$content, { top: 0, left: 0 }); + if (this._$content) { + move(this._$content, { top: 0, left: 0 }); + } this.detectVisualPositionChange(); } else { this._props.forceApplyBindings?.(); @@ -99,14 +101,20 @@ class PopupPositionController extends OverlayPositionController { } } + clean(): void { + this._$dragResizeContainer = undefined; + super.clean(); + } + _updateDragResizeContainer(): void { this._$dragResizeContainer = this._getDragResizeContainer(); } - _getDragResizeContainer(): dxElementWrapper { + _getDragResizeContainer(): dxElementWrapper | undefined { if (this._props.dragOutsideBoundary) { return $(window); } + if (this._props.dragAndResizeArea) { return $(this._props.dragAndResizeArea); } diff --git a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts index 316a68794386..58b4bf08b1c4 100644 --- a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts +++ b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip.ts @@ -62,7 +62,7 @@ class SliderTooltip extends Tooltip { const { value, format } = this.option(); const formattedText = numberLocalization.format(value ?? 0, format); - this.$content().text(formattedText); + this.$content()?.text(formattedText); this._renderPosition(); } diff --git a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts index 41f26a971111..557eaac65181 100644 --- a/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts +++ b/packages/devextreme/js/__internal/ui/slider/m_slider_tooltip_position_controller.ts @@ -41,7 +41,10 @@ class SliderTooltipPositionController extends PopoverPositionController { const isLeftSide = collisionSide === 'left'; const offset = (isLeftSide ? 1 : -1) * oversize; - move(this._$content, { left: left + offset }); + + if (this._$content) { + move(this._$content, { left: left + offset }); + } this._updateVisualPositionValue(); } diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts index bb01578a1906..6272e988d88f 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_item.ts @@ -43,9 +43,9 @@ class SpeedDialItem extends Overlay { _currentVisible?: boolean; - _$wrapper!: dxElementWrapper; + _$wrapper?: dxElementWrapper | null; - _$content!: dxElementWrapper; + _$content?: dxElementWrapper | null; _inkRipple?: any; @@ -82,8 +82,10 @@ class SpeedDialItem extends Overlay { } _moveToContainer(): void { - this._$wrapper.appendTo(this.$element()); - this._$content.appendTo(this._$wrapper); + if (this._$wrapper) { + this._$wrapper.appendTo(this.$element()); + this._$content?.appendTo(this._$wrapper); + } } _render(): void { @@ -114,11 +116,15 @@ class SpeedDialItem extends Overlay { const $element = $('
').addClass(FAB_LABEL_CLASS); const $wrapper = $('
').addClass(FAB_LABEL_WRAPPER_CLASS); - this._$label = $wrapper - .prependTo(this.$content()) - .append($element.text(label)); + const $content = this.$content(); + + if ($content) { + this._$label = $wrapper + .prependTo($content) + .append($element.text(label)); - this.$content().toggleClass(FAB_CONTENT_REVERSE_CLASS, this._isPositionLeft(this.option('parentPosition'))); + $content.toggleClass(FAB_CONTENT_REVERSE_CLASS, this._isPositionLeft(this.option('parentPosition'))); + } } _isPositionLeft(position) { @@ -194,8 +200,8 @@ class SpeedDialItem extends Overlay { _updateZIndexStackPosition(): void { const { zIndex } = this.option(); - this._$wrapper.css('zIndex', zIndex); - this._$content.css('zIndex', zIndex); + this._$wrapper?.css('zIndex', zIndex); + this._$content?.css('zIndex', zIndex); } _setClickAction(): void { @@ -229,7 +235,7 @@ class SpeedDialItem extends Overlay { this._inkRipple = render(); } - _getInkRippleContainer(): dxElementWrapper | undefined { + _getInkRippleContainer(): dxElementWrapper | undefined | null { return this._$icon; } diff --git a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts index be59352c93b6..cedcc091f3b3 100644 --- a/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts +++ b/packages/devextreme/js/__internal/ui/speed_dial_action/m_speed_dial_main_item.ts @@ -239,7 +239,7 @@ class SpeedDialMainItem extends SpeedDialItem { actions[i].option('animation', this._getActionAnimation(actions[i], i, lastActionIndex)); actions[i].option('position', this._getActionPosition(actions, i)); // @ts-expect-error - actions[i]._$wrapper.css('position', this._$wrapper.css('position')); + actions[i]._$wrapper?.css('position', this._$wrapper?.css('position')); actions[i].toggle(); } @@ -353,7 +353,7 @@ class SpeedDialMainItem extends SpeedDialItem { _outsideClickHandler(e): void { if (this._isShadingShown) { - const isShadingClick = $(e.target)[0] === this._$wrapper[0]; + const isShadingClick = $(e.target)[0] === this._$wrapper?.[0]; if (isShadingClick) { e.preventDefault(); @@ -374,7 +374,7 @@ class SpeedDialMainItem extends SpeedDialItem { return this._getDefaultOptions().position; } - _getInkRippleContainer() { + _getInkRippleContainer(): dxElementWrapper | undefined | null { return this.$content(); } diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts index e804aade1cbf..3ca436e68a56 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.base.ts @@ -267,6 +267,7 @@ class TextEditorBase< this._$textEditorInputContainer = $('
') .addClass(TEXTEDITOR_INPUT_CONTAINER_CLASS) .appendTo(this._$textEditorContainer); + this._$textEditorInputContainer.append(this._createInput()); } @@ -277,6 +278,11 @@ class TextEditorBase< _renderPendingIndicator(): void { this.$element().addClass(TEXTEDITOR_VALIDATION_PENDING_CLASS); const $inputContainer = this._getInputContainer(); + + // if (!$inputContainer) { + // return; + // } + const $indicatorElement = $('
') .addClass(TEXTEDITOR_PENDING_INDICATOR_CLASS) .appendTo($inputContainer); @@ -348,11 +354,16 @@ class TextEditorBase< this._buttonCollection.clean(); this._disposePendingIndicator(); this._unobserveLabelContainerResize(); + + super._clean(); + this._$beforeButtonsContainer = null; this._$afterButtonsContainer = null; - // @ts-expect-error ts-error + // @ts-expect-error _$textEditorContainer can be null and undefined this._$textEditorContainer = null; - super._clean(); + // @ts-expect-error _$textEditorInputContainer can be null and undefined + this._$textEditorInputContainer = null; + this._$placeholder = null; } _createInput(): dxElementWrapper { @@ -1013,6 +1024,7 @@ class TextEditorBase< } getButton(name: string): dxButton | undefined | null { + // @ts-expect-error TextEditorButtonCollection should use generic return this._buttonCollection.getButton(name); } diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.clear.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.clear.ts index 89d303507fa2..3a267ef70788 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.clear.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.clear.ts @@ -35,30 +35,27 @@ export default class ClearButton extends TextEditorButton { _isVisible(): boolean { const { editor } = this; - return editor._isClearButtonVisible(); + return !!editor?._isClearButtonVisible(); } - _attachEvents(instance, $button: dxElementWrapper): void { - const { editor } = this; - const editorName = editor.NAME; + _attachEvents(instance: dxElementWrapper, $button: dxElementWrapper): void { + const editorName = this.editor?.NAME ?? ''; eventsEngine.on( $button, - // @ts-expect-error ts-error addNamespace(pointerDown, editorName), (e) => { e.preventDefault(); if (e.pointerType !== 'mouse') { - editor._clearValueHandler(e); + this.editor?._clearValueHandler(e); } }, ); eventsEngine.on( $button, - // @ts-expect-error ts-error addNamespace(click, editorName), - (e) => editor._clearValueHandler(e), + (e) => this.editor?._clearValueHandler(e), ); } @@ -75,13 +72,19 @@ export default class ClearButton extends TextEditorButton { } const { editor, instance } = this; + + if (!editor) { + return; + } + const $editor = editor.$element(); const isVisible = this._isVisible(); if (instance) { - // @ts-expect-error ts-error + // @ts-expect-error instance is dxElementWrapper instance.toggleClass(STATE_INVISIBLE_CLASS, !isVisible); } + this._legacyRender($editor, isVisible); } } diff --git a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts index 79499a33e316..3aa1ce865d48 100644 --- a/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts +++ b/packages/devextreme/js/__internal/ui/text_box/m_text_editor.label.ts @@ -79,6 +79,7 @@ class TextEditorLabel { this._updateEditorBeforeButtonsClass(visible); this._updateEditorLabelClass(visible); + // eslint-disable-next-line @typescript-eslint/no-unused-expressions visible ? this._$root.appendTo(this._props.$editor) : this._$root.detach(); diff --git a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_button.ts b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_button.ts index 00e22f2e7f20..756bba3732d9 100644 --- a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_button.ts +++ b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_button.ts @@ -1,18 +1,24 @@ import type { dxElementWrapper } from '@js/core/renderer'; import $ from '@js/core/renderer'; import type { Properties } from '@js/ui/button'; -import type Button from '@js/ui/button'; +import Button from '@js/ui/button'; import type TextEditorBase from '../m_text_editor.base'; +type TextEditorButtonInstance = dxElementWrapper | Button; + +export const isButtonInstance = ( + instance: unknown, +): instance is Button => instance instanceof Button; + export default class TextEditorButton { $container!: dxElementWrapper; $placeMarker?: dxElementWrapper | null; - instance?: Button | null; + instance?: TextEditorButtonInstance | null; - editor!: TextEditorBase; + editor!: TextEditorBase | null; name!: string; @@ -47,7 +53,7 @@ export default class TextEditorButton { } // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this - _attachEvents(instance: unknown, $element: dxElementWrapper): void { + _attachEvents(instance: unknown, $element?: dxElementWrapper): void { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw 'Not implemented'; } @@ -56,7 +62,7 @@ export default class TextEditorButton { _create(): { instance: Button | dxElementWrapper; $element: dxElementWrapper; - } { + } | undefined { // eslint-disable-next-line @typescript-eslint/no-throw-literal throw 'Not implemented'; } @@ -69,7 +75,7 @@ export default class TextEditorButton { const { editor, options } = this; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return options.visible || !editor.option('readOnly'); + return options.visible || !editor?.option('readOnly'); } // eslint-disable-next-line class-methods-use-this @@ -83,28 +89,34 @@ export default class TextEditorButton { } dispose(): void { - const { instance, $placeMarker } = this; + const { instance } = this; if (instance) { // TODO: instance.dispose() - if (instance.dispose) { + if (isButtonInstance(instance)) { instance.dispose(); + instance.$element().remove(); + // @ts-expect-error _$element is private + instance._$element = null; } else { - // @ts-expect-error ts-error instance.remove(); } - this.instance = null; } - $placeMarker && $placeMarker.remove(); + this.instance = null; + this.editor = null; + // @ts-expect-error $container can be null and undefined + this.$container = null; + this.$placeMarker?.remove(); + this.$placeMarker = null; } render($container: dxElementWrapper = this.$container): void { this.$container = $container; if (this._isVisible()) { - const { instance, $element } = this._create(); - // @ts-expect-error ts-error + const { instance, $element } = this._create() ?? {}; + this.instance = instance; this._attachEvents(instance, $element); } else { diff --git a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_custom.ts b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_custom.ts index 2065b844dcf3..5f1bf1c1f0d8 100644 --- a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_custom.ts +++ b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_custom.ts @@ -6,22 +6,20 @@ import $ from '@js/core/renderer'; import Button from '@js/ui/button'; import type TextEditorBase from '../m_text_editor.base'; -import TextEditorButton from './m_button'; +import TextEditorButton, { isButtonInstance } from './m_button'; const CUSTOM_BUTTON_HOVERED_CLASS = 'dx-custom-button-hovered'; export default class CustomButton extends TextEditorButton { _attachEvents( - instance: unknown, + instance: Button, $element: dxElementWrapper, ): void { - const { editor } = this; - eventsEngine.on($element, start, () => { - editor.$element().addClass(CUSTOM_BUTTON_HOVERED_CLASS); + this.editor?.$element().addClass(CUSTOM_BUTTON_HOVERED_CLASS); }); eventsEngine.on($element, end, () => { - editor.$element().removeClass(CUSTOM_BUTTON_HOVERED_CLASS); + this.editor?.$element().removeClass(CUSTOM_BUTTON_HOVERED_CLASS); }); eventsEngine.on($element, clickEventName, (e) => { e.stopPropagation(); @@ -29,24 +27,33 @@ export default class CustomButton extends TextEditorButton { } _create(): { - $element: dxElementWrapper; instance: Button; - } { + $element: dxElementWrapper; + } | undefined { const { editor } = this; + + if (!editor) { + return undefined; + } + const $element = $('
'); this._addToContainer($element); - const instance = editor._createComponent($element, Button, { - ...this.options, - ignoreParentReadOnly: true, - disabled: this._isDisabled(), - integrationOptions: this._prepareIntegrationOptions(editor), - }); + const instance = editor._createComponent( + $element, + Button, + { + ...this.options, + ignoreParentReadOnly: true, + disabled: this._isDisabled(), + integrationOptions: this._prepareIntegrationOptions(editor), + }, + ); return { - $element, instance, + $element, }; } @@ -61,7 +68,7 @@ export default class CustomButton extends TextEditorButton { update(): boolean { const isUpdated = super.update(); - if (this.instance) { + if (isButtonInstance(this.instance)) { this.instance.option('disabled', this._isDisabled()); } @@ -69,7 +76,7 @@ export default class CustomButton extends TextEditorButton { } _isVisible(): boolean { - const { visible } = this.editor.option(); + const { visible } = this.editor?.option() ?? {}; return !!visible; } @@ -77,13 +84,13 @@ export default class CustomButton extends TextEditorButton { _isDisabled(): boolean | undefined { const isDefinedByUser = this.options.disabled !== undefined; if (isDefinedByUser) { - if (this.instance) { + if (isButtonInstance(this.instance)) { return this.instance.option('disabled'); } return this.options.disabled; } - const { readOnly } = this.editor.option(); + const { readOnly } = this.editor?.option() ?? {}; return readOnly; } diff --git a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_index.ts b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_index.ts index cb136d7d08f5..6b8362356811 100644 --- a/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_index.ts +++ b/packages/devextreme/js/__internal/ui/text_box/texteditor_button_collection/m_index.ts @@ -163,7 +163,7 @@ export default class TextEditorButtonCollection< this.buttons = []; } - getButton(buttonName: string): dxButton | null | undefined { + getButton(buttonName: string): dxElementWrapper | dxButton | null | undefined { const button = this.buttons.find(({ name }) => name === buttonName); return button?.instance; diff --git a/packages/devextreme/js/__internal/ui/toast/m_toast.ts b/packages/devextreme/js/__internal/ui/toast/m_toast.ts index c4756176bf36..dc16b6786118 100644 --- a/packages/devextreme/js/__internal/ui/toast/m_toast.ts +++ b/packages/devextreme/js/__internal/ui/toast/m_toast.ts @@ -168,16 +168,19 @@ TProperties extends ToastProperties = ToastProperties, _renderContentImpl() { const { message, type } = this.option(); - this._message = $('
') - .addClass(TOAST_MESSAGE_CLASS) - // @ts-expect-error ts-error - .text(message) - .appendTo(this.$content()); + const $content = this.$content(); + + if ($content) { + this._message = $('
') + .addClass(TOAST_MESSAGE_CLASS) + .text(message ?? '') + .appendTo($content); + } this.setAria('role', 'alert', this._message); // @ts-expect-error ts-error if (toastTypes.includes(type.toLowerCase())) { - this.$content().prepend($('
').addClass(TOAST_ICON_CLASS)); + $content?.prepend($('
').addClass(TOAST_ICON_CLASS)); } super._renderContentImpl(); @@ -186,12 +189,12 @@ TProperties extends ToastProperties = ToastProperties, _render(): void { super._render(); - this.$element().addClass(TOAST_CLASS); - this.$wrapper().addClass(TOAST_WRAPPER_CLASS); + this.$element()?.addClass(TOAST_CLASS); + this.$wrapper()?.addClass(TOAST_WRAPPER_CLASS); const { type } = this.option(); - this.$content().addClass(TOAST_CLASS_PREFIX + String(type).toLowerCase()); - this.$content().addClass(TOAST_CONTENT_CLASS); + this.$content()?.addClass(TOAST_CLASS_PREFIX + String(type).toLowerCase()); + this.$content()?.addClass(TOAST_CONTENT_CLASS); this._toggleCloseEvents('Swipe'); this._toggleCloseEvents('Click'); @@ -259,8 +262,8 @@ TProperties extends ToastProperties = ToastProperties, switch (name) { case 'type': - this.$content().removeClass(TOAST_CLASS_PREFIX + previousValue); - this.$content().addClass(TOAST_CLASS_PREFIX + String(value).toLowerCase()); + this.$content()?.removeClass(TOAST_CLASS_PREFIX + previousValue); + this.$content()?.addClass(TOAST_CLASS_PREFIX + String(value).toLowerCase()); break; case 'message': if (this._message) { diff --git a/packages/devextreme/js/ui/diagram/ui.diagram.dialogs.js b/packages/devextreme/js/ui/diagram/ui.diagram.dialogs.js index 63aed4ebbf5d..da7f266497d7 100644 --- a/packages/devextreme/js/ui/diagram/ui.diagram.dialogs.js +++ b/packages/devextreme/js/ui/diagram/ui.diagram.dialogs.js @@ -101,8 +101,8 @@ class DiagramDialog extends Widget { } _show() { this._popup - .$content() - .empty() + ?.$content() + ?.empty() .append(this._onGetContentAction()); this._popup.show(); this._isShown = true; diff --git a/packages/devextreme/js/ui/file_manager/ui.file_manager.notification.js b/packages/devextreme/js/ui/file_manager/ui.file_manager.notification.js index 1932fbe94220..ce1ac55141e6 100644 --- a/packages/devextreme/js/ui/file_manager/ui.file_manager.notification.js +++ b/packages/devextreme/js/ui/file_manager/ui.file_manager.notification.js @@ -239,7 +239,7 @@ export default class FileManagerNotificationControl extends Widget { if(this._isProgressDrawerOpened() || !this.option('showNotificationPopup')) { return; } - this._getNotificationPopup().$wrapper().toggleClass(FILE_MANAGER_NOTIFICATION_POPUP_ERROR_CLASS, !!errorMode); + this._getNotificationPopup().$wrapper()?.toggleClass(FILE_MANAGER_NOTIFICATION_POPUP_ERROR_CLASS, !!errorMode); this._getNotificationPopup().option('contentTemplate', content); if(!this._getNotificationPopup().option('visible')) { this._getNotificationPopup().show(); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js index b8eb1bcf4e68..f62735635d87 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/dropDownEditor.tests.js @@ -2677,3 +2677,55 @@ QUnit.module('aria accessibility', () => { }); }); }); + +QUnit.module('Memory Leaks', { + beforeEach: function() { + this.$element = $('
').appendTo('#qunit-fixture'); + this.getButton = (instance, name) => instance._buttonCollection.buttons.find(button => button.name === name); + }, + afterEach: function() { + this.$element.remove(); + } +}, () => { + QUnit.test('should clear popup and template references on dispose', function(assert) { + const instance = this.$element.dxDropDownEditor({ + opened: true, + fieldTemplate: () => { + return $('
').dxTextBox({ value: 'Custom Field' }); + }, + }).dxDropDownEditor('instance'); + + assert.notStrictEqual(instance._$templateWrapper, undefined, '_$templateWrapper exists before dispose'); + assert.notStrictEqual(instance.content(), null, 'content() returns popup content before dispose'); + assert.notStrictEqual(instance._popup, undefined, '_popup instance exists before dispose'); + assert.notStrictEqual(instance._$popup, undefined, '_$popup element exists before dispose'); + + instance.dispose(); + + assert.strictEqual(instance._$templateWrapper, undefined, '_$templateWrapper is undefined after dispose'); + assert.strictEqual(instance.content(), null, 'content() returns null after dispose'); + assert.strictEqual(instance._popup, undefined, '_popup is undefined after dispose'); + assert.strictEqual(instance._$popup, undefined, '_$popup is undefined after dispose'); + }); + + [ + 'editor', + '$container', + 'instance', + '$placeMarker', + ].forEach((property) => { + QUnit.test(`DropDownButton should clear ${property} reference on dispose`, function(assert) { + const instance = this.$element.dxDropDownEditor({ + showDropDownButton: property !== '$placeMarker', + }).dxDropDownEditor('instance'); + + const dropDownButton = this.getButton(instance, 'dropDown'); + + assert.notStrictEqual(dropDownButton[property], property === '$placeMarker' ? null : undefined, `button has ${property} reference before dispose`); + + instance.dispose(); + + assert.strictEqual(dropDownButton[property], null, `button.${property} is null after dispose`); + }); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js index 73b8dc336973..488f8ea8c570 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/textEditorParts/common.tests.js @@ -1624,3 +1624,92 @@ QUnit.module('validation', () => { assert.strictEqual($('.dx-invalid-message-content').attr('id'), messageId, 'message content id is correct'); }); }); + +QUnit.module('TextEditorButton - Memory Leaks', { + beforeEach: function() { + this.$element = $('
').appendTo('#qunit-fixture'); + this.getButton = (instance, name) => instance._buttonCollection.buttons.find(button => button.name === name); + }, + afterEach: function() { + this.$element.remove(); + } +}, () => { + [ + 'editor', + '$container', + 'instance', + ].forEach((property) => { + QUnit.test(`should clear ${property} reference on dispose`, function(assert) { + const instance = this.$element.dxTextEditor({ + showClearButton: true, + value: 'test', + }).dxTextEditor('instance'); + + const clearButton = this.getButton(instance, 'clear'); + + assert.notStrictEqual(clearButton[property], undefined, `button has ${property} reference before dispose`); + + instance.dispose(); + + assert.strictEqual(clearButton[property], null, `button.${property} is null after dispose`); + }); + }); + + QUnit.test('should clear $placeMarker reference on dispose', function(assert) { + const instance = this.$element.dxTextEditor({ + showClearButton: true, + value: 'test', + readOnly: true, + }).dxTextEditor('instance'); + + const clearButton = this.getButton(instance, 'clear'); + + assert.notStrictEqual(clearButton.$placeMarker, null, 'button has $placeMarker reference before dispose'); + + instance.dispose(); + + assert.strictEqual(clearButton.$placeMarker, null, 'button.$placeMarker is null after dispose'); + }); +}); + +QUnit.module('TextEditorBase - Memory Leaks', { + beforeEach: function() { + this.$element = $('
').appendTo('#qunit-fixture'); + }, + afterEach: function() { + this.$element.remove(); + } +}, () => { + QUnit.test('should clear DOM element references on dispose', function(assert) { + const instance = this.$element.dxTextEditor({ + value: 'test', + placeholder: 'Enter text', + buttons: [ + { + name: 'custom1', + location: 'after', + options: { icon: 'search' }, + }, + { + name: 'custom2', + location: 'before', + options: { icon: 'search' }, + }, + ] + }).dxTextEditor('instance'); + + assert.notStrictEqual(instance._$beforeButtonsContainer, null, '_$beforeButtonsContainer exists before dispose'); + assert.notStrictEqual(instance._$afterButtonsContainer, null, '_$afterButtonsContainer exists before dispose'); + assert.notStrictEqual(instance._$textEditorContainer, undefined, '_$textEditorContainer exists before dispose'); + assert.notStrictEqual(instance._$textEditorInputContainer, undefined, '_$textEditorInputContainer exists before dispose'); + assert.notStrictEqual(instance._$placeholder, null, '_$placeholder exists before dispose'); + + instance.dispose(); + + assert.strictEqual(instance._$beforeButtonsContainer, null, '_$beforeButtonsContainer is null after dispose'); + assert.strictEqual(instance._$afterButtonsContainer, null, '_$afterButtonsContainer is null after dispose'); + assert.strictEqual(instance._$textEditorContainer, null, '_$textEditorContainer is null after dispose'); + assert.strictEqual(instance._$textEditorInputContainer, null, '_$textEditorInputContainer is null after dispose'); + assert.strictEqual(instance._$placeholder, null, '_$placeholder is null after dispose'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js index 1c8404f8b9c4..ec885d722a4d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/overlay.tests.js @@ -2216,6 +2216,27 @@ testModule('close on outside click', moduleConfig, () => { assert.strictEqual(overlay2.option('visible'), false, 'Second overlay is hidden, because of outsideclick'); }); + test('overlay should handle outside click without errors when content is null', function(assert) { + const overlay = $('#overlay').dxOverlay({ + hideOnOutsideClick: true, + visible: true, + }).dxOverlay('instance'); + + const originalContent = overlay._$content; + + overlay._$content = null; + + try { + $(document).trigger('dxpointerdown'); + assert.ok(true, 'No error when content is null'); + assert.strictEqual(overlay.option('visible'), false, 'Overlay should hide on outside click when content is null'); + } catch(e) { + assert.ok(false, `Error occurred with null content: ${e.message}`); + } finally { + overlay._$content = originalContent; + } + }); + test('document events should be unsubscribed at each overlay hiding', function(assert) { const $overlay1 = $('#overlay').dxOverlay({ [closeOnOutsideClickOptionName]: true, @@ -4396,3 +4417,59 @@ QUnit.module('wrapper covered element choice', { assert.roughEqual(wrapperLocation.top, containerLocation.top, 0.51, 'wrapper is top positioned by position.of'); }); }); + +QUnit.module('Memory Leaks', { + beforeEach: function() { + this.clock = sinon.useFakeTimers(); + this.$element = $('#overlay'); + + this.getPositionController = (instance) => { + return instance._positionController; + }; + }, + afterEach: function() { + this.$element.remove(); + this.clock.restore(); + } +}, () => { + QUnit.test('should clear _$wrapper reference on dispose', function(assert) { + const instance = new Overlay(this.$element, { visible: true }); + + this.clock.tick(0); + + assert.notStrictEqual(instance.$wrapper(), null, 'wrapper exists before dispose'); + + instance.dispose(); + + assert.strictEqual(instance.$wrapper(), null, '$wrapper() returns null after dispose'); + }); + + QUnit.test('should clear _$content reference on dispose', function(assert) { + const instance = new Overlay(this.$element, { visible: true }); + + this.clock.tick(0); + + assert.notStrictEqual(instance.$content(), null, 'content exists before dispose'); + + instance.dispose(); + + assert.strictEqual(instance.$content(), null, '$content() returns null after dispose'); + }); + + QUnit.test('should clear PositionController references on dispose', function(assert) { + const instance = new Overlay(this.$element, { visible: true }); + + this.clock.tick(0); + + const positionController = this.getPositionController(instance); + assert.ok(positionController._$content, 'PositionController._$content exists before dispose'); + + instance.dispose(); + + assert.strictEqual(positionController._$content, undefined, 'PositionController._$content is undefined after dispose'); + assert.strictEqual(positionController._$wrapper, undefined, 'PositionController._$wrapper is undefined after dispose'); + assert.strictEqual(positionController._$root, undefined, 'PositionController._$root is undefined after dispose'); + assert.strictEqual(positionController._$markupContainer, undefined, 'PositionController._$markupContainer is undefined after dispose'); + assert.strictEqual(positionController._$visualContainer, undefined, 'PositionController._$visualContainer is undefined after dispose'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js index 75b357e49fc6..182dbd256312 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/popup.tests.js @@ -3277,3 +3277,85 @@ QUnit.module('positioning', { }); }); }); + +QUnit.module('Memory Leaks', { + beforeEach: function() { + executeAsyncMock.setup(); + this.$element = $('#popup'); + + this.getPositionController = (instance) => { + return instance._positionController; + }; + }, + afterEach: function() { + this.$element.remove(); + executeAsyncMock.teardown(); + } +}, () => { + QUnit.test('should clear topToolbar reference on dispose', function(assert) { + const instance = $('#popup').dxPopup({ + visible: true, + title: 'Test Title', + }).dxPopup('instance'); + + assert.notStrictEqual(instance.topToolbar(), undefined, 'topToolbar exists before dispose'); + + instance.dispose(); + + assert.strictEqual(instance.topToolbar(), undefined, 'topToolbar() returns undefined after dispose'); + }); + + QUnit.test('should clear bottomToolbar reference on dispose', function(assert) { + const instance = $('#popup').dxPopup({ + visible: true, + toolbarItems: [{ + toolbar: 'bottom', + widget: 'dxButton', + options: { text: 'OK' }, + }], + }).dxPopup('instance'); + + assert.notStrictEqual(instance.bottomToolbar(), undefined, 'bottomToolbar exists before dispose'); + + instance.dispose(); + + assert.strictEqual(instance.bottomToolbar(), undefined, 'bottomToolbar() returns undefined after dispose'); + }); + + QUnit.test('should clear $content reference on dispose', function(assert) { + const instance = $('#popup').dxPopup({ + visible: true, + contentTemplate: function() { + return $('
').text('Test Content'); + }, + }).dxPopup('instance'); + + assert.notStrictEqual(instance.$content(), undefined, 'content exists before dispose'); + + instance.dispose(); + + assert.strictEqual(instance.$content(), undefined, '$content() returns undefined after dispose'); + }); + + QUnit.test('should clear PopupPositionController references on dispose', function(assert) { + const instance = $('#popup').dxPopup({ + visible: true, + dragEnabled: true, + }).dxPopup('instance'); + + const positionController = this.getPositionController(instance); + + assert.ok(positionController._$content, 'PositionController._$content exists before dispose'); + + instance.dispose(); + + assert.strictEqual( + positionController._$dragResizeContainer, + undefined, + 'PopupPositionController._$dragResizeContainer is undefined after dispose', + ); + assert.strictEqual(positionController._$content, undefined, 'PositionController._$content is undefined after dispose'); + assert.strictEqual(positionController._$wrapper, undefined, 'PositionController._$wrapper is undefined after dispose'); + assert.strictEqual(positionController._$root, undefined, 'PositionController._$root is undefined after dispose'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui/eventsStrategy.tests.js b/packages/devextreme/testing/tests/DevExpress.ui/eventsStrategy.tests.js index 5ad5865800f0..4eda27b56df8 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui/eventsStrategy.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui/eventsStrategy.tests.js @@ -102,4 +102,22 @@ QUnit.test('callbacks should have correct context', function(assert) { $element.remove(); }); - +QUnit.module('Memory leaks', { + beforeEach: function() { + const markup = '
'; + $('#qunit-fixture').html(markup); + }, + afterEach: function() { + $('#element').remove(); + } +}, () => { + QUnit.test('should clear _owner reference after dispose', function(assert) { + const strategy = new EventsStrategy({}); + + assert.notStrictEqual(strategy._owner, null, '_owner is set before dispose'); + + strategy.dispose(); + + assert.strictEqual(strategy._owner, null, '_owner is null after dispose'); + }); +});