Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
}
Expand Down
50 changes: 31 additions & 19 deletions packages/devextreme/js/__internal/ui/list/list.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ export interface ListBaseProperties extends Properties<Item>, Omit<

_onItemsRendered?: () => void;

_swipeEnabled?: boolean;

showChevronExpr?: (data: Item) => boolean | undefined;

badgeExpr?: (data: Item) => string | undefined;
Expand All @@ -134,16 +132,12 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {

_$nextButton!: dxElementWrapper | null;

// eslint-disable-next-line no-restricted-globals
_holdTimer?: ReturnType<typeof setTimeout>;

// eslint-disable-next-line no-restricted-globals
_loadNextPageTimer?: ReturnType<typeof setTimeout>;

// eslint-disable-next-line no-restricted-globals
_showLoadingIndicatorTimer?: ReturnType<typeof setTimeout>;

// eslint-disable-next-line no-restricted-globals
_inkRippleTimer?: ReturnType<typeof setTimeout>;

_isFirstLoadCompleted?: boolean;
Expand Down Expand Up @@ -301,7 +295,6 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
_itemAttributes: { role: 'option' },
useInkRipple: false,
wrapItemText: false,
_swipeEnabled: true,
showChevronExpr(data: Item): boolean | undefined {
return data?.showChevron;
},
Expand Down Expand Up @@ -1074,26 +1067,23 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
_postprocessRenderItem(args: PostprocessRenderItemInfo<Item>): 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 {
Expand All @@ -1102,6 +1092,29 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
});
}

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;
}

Copy link
Contributor

@pharret31 pharret31 Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are there any tests that checks resubscriptions in existing tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have test which checks onItemSwipe subscription by on method

QUnit.test('onItemSwipe - subscription by on method', function(assert) {

_nextButtonHandler(): void {
const pageLoadingArgs = {
component: this as unknown as dxList,
Expand Down Expand Up @@ -1408,7 +1421,6 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
case 'badgeExpr':
this._invalidate();
break;
case '_swipeEnabled':
case '_onItemsRendered':
case 'selectByClick':
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);

Expand All @@ -1188,7 +1166,7 @@ QUnit.module('options changed', moduleSetup, () => {
list.off('itemSwipe');
swipeItem();

list.on('itemSwipe', swipeHandler);
list.on({ 'itemSwipe': swipeHandler });
swipeItem();
});

Expand Down Expand Up @@ -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) => {
Expand Down
Loading