From 318fc4c364a443022974bd49c8e5d581d1589e3d Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 25 Feb 2026 16:42:40 +0400 Subject: [PATCH 1/3] Add checks to ensure element exists --- .../__internal/common/core/animation/m_position.ts | 9 +++++++++ .../__internal/common/core/animation/translator.ts | 14 +++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/devextreme/js/__internal/common/core/animation/m_position.ts b/packages/devextreme/js/__internal/common/core/animation/m_position.ts index 77c38e6bb32f..af494e074830 100644 --- a/packages/devextreme/js/__internal/common/core/animation/m_position.ts +++ b/packages/devextreme/js/__internal/common/core/animation/m_position.ts @@ -399,6 +399,10 @@ const getOffsetWithoutScale = function ($startElement, $currentElement = $startE const position = function (what, options?) { const $what = $(what); + if (!$what.length) { + return undefined; + } + if (!options) { return $what.offset(); } @@ -406,6 +410,11 @@ const position = function (what, options?) { resetPosition($what, true); const offset = getOffsetWithoutScale($what); + + if (!offset) { + return undefined; + } + const targetPosition = options.h && options.v ? options : calculatePosition($what, options); const preciser = function (number) { diff --git a/packages/devextreme/js/__internal/common/core/animation/translator.ts b/packages/devextreme/js/__internal/common/core/animation/translator.ts index fe83ff5cb1a1..a19a3646976e 100644 --- a/packages/devextreme/js/__internal/common/core/animation/translator.ts +++ b/packages/devextreme/js/__internal/common/core/animation/translator.ts @@ -100,6 +100,10 @@ export const move = function ( // eslint-disable-next-line no-param-reassign $element = $($element); + if (!position) { + return; + } + const { left, top } = position; // eslint-disable-next-line @typescript-eslint/init-declarations let translate; @@ -150,9 +154,13 @@ export const resetPosition = function ( clearCache($element); if (finishTransition) { - // @ts-expect-error - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - $element.get(0).offsetHeight; + const element = $element.get(0); + + if (element && 'offsetHeight' in element) { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + element.offsetHeight; + } + $element.css('transition', originalTransition); } }; From e08cb7b017f6965efbdb87034d0364bfa09c9d83 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Wed, 25 Feb 2026 18:19:57 +0400 Subject: [PATCH 2/3] cleaner version of the fix --- .../js/__internal/common/core/animation/m_position.ts | 5 ----- .../js/__internal/common/core/animation/translator.ts | 11 ----------- 2 files changed, 16 deletions(-) diff --git a/packages/devextreme/js/__internal/common/core/animation/m_position.ts b/packages/devextreme/js/__internal/common/core/animation/m_position.ts index af494e074830..0c4089d10c8d 100644 --- a/packages/devextreme/js/__internal/common/core/animation/m_position.ts +++ b/packages/devextreme/js/__internal/common/core/animation/m_position.ts @@ -410,11 +410,6 @@ const position = function (what, options?) { resetPosition($what, true); const offset = getOffsetWithoutScale($what); - - if (!offset) { - return undefined; - } - const targetPosition = options.h && options.v ? options : calculatePosition($what, options); const preciser = function (number) { diff --git a/packages/devextreme/js/__internal/common/core/animation/translator.ts b/packages/devextreme/js/__internal/common/core/animation/translator.ts index a19a3646976e..001375a94dc6 100644 --- a/packages/devextreme/js/__internal/common/core/animation/translator.ts +++ b/packages/devextreme/js/__internal/common/core/animation/translator.ts @@ -100,10 +100,6 @@ export const move = function ( // eslint-disable-next-line no-param-reassign $element = $($element); - if (!position) { - return; - } - const { left, top } = position; // eslint-disable-next-line @typescript-eslint/init-declarations let translate; @@ -154,13 +150,6 @@ export const resetPosition = function ( clearCache($element); if (finishTransition) { - const element = $element.get(0); - - if (element && 'offsetHeight' in element) { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - element.offsetHeight; - } - $element.css('transition', originalTransition); } }; From 571bde1fc89d8da62d5a300b2ca5c6df171362f7 Mon Sep 17 00:00:00 2001 From: Arman Jivanyan Date: Tue, 10 Mar 2026 11:25:51 +0400 Subject: [PATCH 3/3] Add tests --- .../common/core/animation/m_position.test.ts | 37 +++++++++++++++++++ .../common/core/animation/translator.test.ts | 34 +++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 packages/devextreme/js/__internal/common/core/animation/m_position.test.ts create mode 100644 packages/devextreme/js/__internal/common/core/animation/translator.test.ts diff --git a/packages/devextreme/js/__internal/common/core/animation/m_position.test.ts b/packages/devextreme/js/__internal/common/core/animation/m_position.test.ts new file mode 100644 index 000000000000..20da271eba0b --- /dev/null +++ b/packages/devextreme/js/__internal/common/core/animation/m_position.test.ts @@ -0,0 +1,37 @@ +import { + describe, expect, it, +} from '@jest/globals'; +import $ from '@js/core/renderer'; + +import positionUtils from './m_position'; + +describe('position (setup)', () => { + describe('when called with an element that does not exist in the DOM', () => { + it('should return undefined without throwing for a selector that matches nothing', () => { + const result = positionUtils.setup('#non-existent-element-that-was-unmounted'); + + expect(result).toBeUndefined(); + }); + + it('should return undefined without throwing for an empty jQuery object', () => { + const $emptyElement = $([]); + + const result = positionUtils.setup($emptyElement); + + expect(result).toBeUndefined(); + }); + }); + + describe('when called with an existing element as a getter (no options)', () => { + it('should return the offset of the element', () => { + const el = document.createElement('div'); + document.body.appendChild(el); + + const result = positionUtils.setup(el); + + expect(result).toBeDefined(); + + document.body.removeChild(el); + }); + }); +}); diff --git a/packages/devextreme/js/__internal/common/core/animation/translator.test.ts b/packages/devextreme/js/__internal/common/core/animation/translator.test.ts new file mode 100644 index 000000000000..6b0f5ec68dd5 --- /dev/null +++ b/packages/devextreme/js/__internal/common/core/animation/translator.test.ts @@ -0,0 +1,34 @@ +import { + describe, expect, it, +} from '@jest/globals'; +import $ from '@js/core/renderer'; + +import { resetPosition } from './translator'; + +describe('resetPosition', () => { + describe('when called with an empty jQuery wrapper and finishTransition=true', () => { + it('should not throw when element does not exist in the DOM', () => { + const $emptyElement = $([]); + + expect(() => resetPosition($emptyElement, true)).not.toThrow(); + }); + + it('should not throw when element selector matches nothing', () => { + const $missingElement = $('#non-existent-element-that-was-unmounted'); + + expect(() => resetPosition($missingElement, true)).not.toThrow(); + }); + }); + + describe('when called with a real DOM element and finishTransition=true', () => { + it('should not throw and should reset position', () => { + const el = document.createElement('div'); + document.body.appendChild(el); + const $el = $(el); + + expect(() => resetPosition($el, true)).not.toThrow(); + + document.body.removeChild(el); + }); + }); +});