diff --git a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts index c50a397c14df..dfe055939599 100644 --- a/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts +++ b/packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts @@ -140,7 +140,6 @@ export class TooltipStrategyBase { onItemClick: (e) => this.onListItemClick(e), onItemContextMenu: this.onListItemContextMenu.bind(this), itemTemplate: (item, index) => this.renderTemplate(item.appointment, item.targetedAppointment, index, item.color), - _swipeEnabled: false, pageLoadMode: 'scrollBottom', }; } diff --git a/packages/devextreme/js/__internal/ui/list/list.base.ts b/packages/devextreme/js/__internal/ui/list/list.base.ts index 80dad79b1d6c..7e851d78a5d4 100644 --- a/packages/devextreme/js/__internal/ui/list/list.base.ts +++ b/packages/devextreme/js/__internal/ui/list/list.base.ts @@ -110,8 +110,6 @@ export interface ListBaseProperties extends Properties, Omit< _onItemsRendered?: () => void; - _swipeEnabled?: boolean; - showChevronExpr?: (data: Item) => boolean | undefined; badgeExpr?: (data: Item) => string | undefined; @@ -134,16 +132,12 @@ export class ListBase extends CollectionWidget { _$nextButton!: dxElementWrapper | null; - // eslint-disable-next-line no-restricted-globals _holdTimer?: ReturnType; - // eslint-disable-next-line no-restricted-globals _loadNextPageTimer?: ReturnType; - // eslint-disable-next-line no-restricted-globals _showLoadingIndicatorTimer?: ReturnType; - // eslint-disable-next-line no-restricted-globals _inkRippleTimer?: ReturnType; _isFirstLoadCompleted?: boolean; @@ -301,7 +295,6 @@ export class ListBase extends CollectionWidget { _itemAttributes: { role: 'option' }, useInkRipple: false, wrapItemText: false, - _swipeEnabled: true, showChevronExpr(data: Item): boolean | undefined { return data?.showChevron; }, @@ -1074,26 +1067,23 @@ export class ListBase extends CollectionWidget { _postprocessRenderItem(args: PostprocessRenderItemInfo): void { this._refreshItemElements(); super._postprocessRenderItem(args); - - // eslint-disable-next-line @typescript-eslint/naming-convention - const { _swipeEnabled } = this.option(); - - if (_swipeEnabled) { - this._attachSwipeEvent($(args.itemElement)); - } + this._updateSwipeEventSubscription($(args.itemElement)); } _getElementClassToSkipRefreshId(): string { return LIST_GROUP_HEADER_CLASS; } - _attachSwipeEvent($itemElement: dxElementWrapper): void { + _updateSwipeEventSubscription($itemElement: dxElementWrapper = this._itemElements()): void { // @ts-expect-error ts-error const endEventName = addNamespace(swipeEventEnd, this.NAME); + eventsEngine.off($itemElement, endEventName); - eventsEngine.on($itemElement, endEventName, (e) => { - this._itemSwipeEndHandler(e); - }); + if (this.hasActionSubscription('onItemSwipe')) { + eventsEngine.on($itemElement, endEventName, (e) => { + this._itemSwipeEndHandler(e); + }); + } } _itemSwipeEndHandler(e: DxEvent & { offset: number }): void { @@ -1102,6 +1092,29 @@ export class ListBase extends CollectionWidget { }); } + on(eventName: string | { [key: string]: Function }, eventHandler?: Function): this { + const result = super.on(eventName, eventHandler); + + const hasItemSwipeHandler = eventName === 'itemSwipe' + || (isPlainObject(eventName) && Object.prototype.hasOwnProperty.call(eventName, 'itemSwipe')); + + if (hasItemSwipeHandler) { + this._updateSwipeEventSubscription(); + } + + return result; + } + + off(eventName: string, eventHandler?: Function): this { + const result = super.off(eventName, eventHandler); + + if (eventName === 'itemSwipe') { + this._updateSwipeEventSubscription(); + } + + return result; + } + _nextButtonHandler(): void { const pageLoadingArgs = { component: this as unknown as dxList, @@ -1408,7 +1421,6 @@ export class ListBase extends CollectionWidget { case 'badgeExpr': this._invalidate(); break; - case '_swipeEnabled': case '_onItemsRendered': case 'selectByClick': break; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js index 16c2613fd9d0..5f18c3116e4d 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js @@ -116,13 +116,12 @@ QUnit.test('contentTemplate passed to createComponent should work correct', asyn assert.equal(stubCreateComponent.getCall(1).args[0][0].nodeName, 'DIV'); assert.equal(stubCreateComponent.getCall(1).args[1], List); - assert.equal(Object.keys(stubCreateComponent.getCall(1).args[2]).length, 8); + assert.equal(Object.keys(stubCreateComponent.getCall(1).args[2]).length, 7); assert.equal(stubCreateComponent.getCall(1).args[2].dataSource, dataList); assert.equal(stubCreateComponent.getCall(1).args[2].showScrollbar, 'onHover'); assert.ok(stubCreateComponent.getCall(1).args[2].onContentReady); assert.ok(stubCreateComponent.getCall(1).args[2].onItemClick); assert.ok(stubCreateComponent.getCall(1).args[2].itemTemplate); - assert.notOk(stubCreateComponent.getCall(1).args[2]._swipeEnabled); assert.equal(stubCreateComponent.getCall(1).args[2].pageLoadMode, 'scrollBottom'); // T1062566 } finally { support.touch = _touch; diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js index 28294779d0a6..873cb6532e70 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js @@ -24,6 +24,7 @@ import eventsEngine from 'common/core/events/core/events_engine'; import ariaAccessibilityTestHelper from '../../../helpers/ariaAccessibilityTestHelper.js'; const LIST_ITEM_CLASS = 'dx-list-item'; +const LIST_ITEM_CONTENT_CLASS = 'dx-list-item-content'; const LIST_ITEMS_CLASS = 'dx-list-items'; const LIST_GROUP_CLASS = 'dx-list-group'; const LIST_GROUP_HEADER_CLASS = 'dx-list-group-header'; @@ -1139,29 +1140,6 @@ QUnit.module('options changed', moduleSetup, () => { swipeItem(); }); - QUnit.test('onItemSwipe handler should not be triggered if "_swipeEnabled" is false on init', function(assert) { - assert.expect(0); - - const swipeHandler = () => { - assert.ok(true, 'swipe handled'); - }; - - this.element.dxList({ - items: [0], - onItemSwipe: swipeHandler, - _swipeEnabled: false - }).dxList('instance'); - - const item = $.proxy(function() { - return this.element.find(`.${LIST_ITEM_CLASS}`).eq(0); - }, this); - const swipeItem = () => { - pointerMock(item()).start().swipeStart().swipe(0.5).swipeEnd(1); - }; - - swipeItem(); - }); - QUnit.test('onItemSwipe - subscription by on method', function(assert) { assert.expect(2); @@ -1188,7 +1166,7 @@ QUnit.module('options changed', moduleSetup, () => { list.off('itemSwipe'); swipeItem(); - list.on('itemSwipe', swipeHandler); + list.on({ 'itemSwipe': swipeHandler }); swipeItem(); }); @@ -4792,6 +4770,94 @@ QUnit.module('Search', () => { }); }); +QUnit.module('Highlighting/selecting', { ...moduleSetup, afterEach: function() { + moduleSetup.afterEach.call(this); + window.getSelection().removeAllRanges(); +} }, () => { + + const selectTextNodePart = (textNode, startOffset, endOffset) => { + const selection = window.getSelection(); + const range = document.createRange(); + selection.removeAllRanges(); + range.setStart(textNode, startOffset); + range.setEnd(textNode, endOffset); + selection.addRange(range); + return selection; + }; + + const getFirstListItemAndTextNode = ($list) => { + const $item = $list.find(`.${LIST_ITEM_CLASS}`).eq(0); + const textNode = $item.find(`.${LIST_ITEM_CONTENT_CLASS}`).eq(0).get(0).firstChild; + + return { $item, textNode }; + }; + + QUnit.test('text selection should not be cleared when dragging on list item without onItemSwipe', function(assert) { + this.element.dxList({ + items: ['Item 1', 'Item 2'], + }); + + const { $item, textNode } = getFirstListItemAndTextNode(this.element); + assert.strictEqual(!!textNode, true, 'text node found in list item'); + + selectTextNodePart(textNode, 0, 4); + assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag'); + + pointerMock($item).start().down(0, 0).move(50, 0).up(); + + assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists after drag'); + }); + + QUnit.test('text selection should be preserved after onItemSwipe handler is removed from options', function(assert) { + this.element.dxList({ + items: ['Item 1', 'Item 2'], + onItemSwipe: sinon.spy(), + }); + const list = this.element.dxList('instance'); + + list.option('onItemSwipe', null); + + const { $item, textNode } = getFirstListItemAndTextNode(this.element); + assert.strictEqual(!!textNode, true, 'text node found in list item'); + + selectTextNodePart(textNode, 0, 4); + assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag'); + + pointerMock($item).start().down(0, 0).move(50, 0).up(); + + assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists after drag'); + }); + + QUnit.test('text selection should reflect itemSwipe on/off subscription state', function(assert) { + this.element.dxList({ + items: ['Item 1', 'Item 2'], + }); + + const list = this.element.dxList('instance'); + + const { $item, textNode } = getFirstListItemAndTextNode(this.element); + assert.strictEqual(!!textNode, true, 'text node found in list item'); + + list.on('itemSwipe', sinon.spy()); + + selectTextNodePart(textNode, 0, 4); + assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag with subscribed swipe handler'); + + pointerMock($item).start().down(0, 0).move(50, 0).up(); + + assert.strictEqual(window.getSelection().toString(), '', 'text selection is cleared while swipe handler is attached'); + + list.off('itemSwipe'); + + selectTextNodePart(textNode, 0, 4); + assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag'); + + pointerMock($item).start().down(0, 0).move(50, 0).up(); + + assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists after drag when swipe handler is removed'); + }); +}); + let helper; if(devices.real().deviceType === 'desktop') { [true, false].forEach((searchEnabled) => {