Skip to content

Commit edb1a98

Browse files
authored
List: Allow selection/highlighting of text in an item [QW] \ Implementation (#32840)
1 parent 76a85f4 commit edb1a98

File tree

4 files changed

+122
-46
lines changed

4 files changed

+122
-46
lines changed

packages/devextreme/js/__internal/scheduler/tooltip_strategies/m_tooltip_strategy_base.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,6 @@ export class TooltipStrategyBase {
140140
onItemClick: (e) => this.onListItemClick(e),
141141
onItemContextMenu: this.onListItemContextMenu.bind(this),
142142
itemTemplate: (item, index) => this.renderTemplate(item.appointment, item.targetedAppointment, index, item.color),
143-
_swipeEnabled: false,
144143
pageLoadMode: 'scrollBottom',
145144
};
146145
}

packages/devextreme/js/__internal/ui/list/list.base.ts

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ export interface ListBaseProperties extends Properties<Item>, Omit<
110110

111111
_onItemsRendered?: () => void;
112112

113-
_swipeEnabled?: boolean;
114-
115113
showChevronExpr?: (data: Item) => boolean | undefined;
116114

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

135133
_$nextButton!: dxElementWrapper | null;
136134

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

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

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

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

149143
_isFirstLoadCompleted?: boolean;
@@ -301,7 +295,6 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
301295
_itemAttributes: { role: 'option' },
302296
useInkRipple: false,
303297
wrapItemText: false,
304-
_swipeEnabled: true,
305298
showChevronExpr(data: Item): boolean | undefined {
306299
return data?.showChevron;
307300
},
@@ -1074,26 +1067,23 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
10741067
_postprocessRenderItem(args: PostprocessRenderItemInfo<Item>): void {
10751068
this._refreshItemElements();
10761069
super._postprocessRenderItem(args);
1077-
1078-
// eslint-disable-next-line @typescript-eslint/naming-convention
1079-
const { _swipeEnabled } = this.option();
1080-
1081-
if (_swipeEnabled) {
1082-
this._attachSwipeEvent($(args.itemElement));
1083-
}
1070+
this._updateSwipeEventSubscription($(args.itemElement));
10841071
}
10851072

10861073
_getElementClassToSkipRefreshId(): string {
10871074
return LIST_GROUP_HEADER_CLASS;
10881075
}
10891076

1090-
_attachSwipeEvent($itemElement: dxElementWrapper): void {
1077+
_updateSwipeEventSubscription($itemElement: dxElementWrapper = this._itemElements()): void {
10911078
// @ts-expect-error ts-error
10921079
const endEventName = addNamespace(swipeEventEnd, this.NAME);
1080+
eventsEngine.off($itemElement, endEventName);
10931081

1094-
eventsEngine.on($itemElement, endEventName, (e) => {
1095-
this._itemSwipeEndHandler(e);
1096-
});
1082+
if (this.hasActionSubscription('onItemSwipe')) {
1083+
eventsEngine.on($itemElement, endEventName, (e) => {
1084+
this._itemSwipeEndHandler(e);
1085+
});
1086+
}
10971087
}
10981088

10991089
_itemSwipeEndHandler(e: DxEvent & { offset: number }): void {
@@ -1102,6 +1092,29 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
11021092
});
11031093
}
11041094

1095+
on(eventName: string | { [key: string]: Function }, eventHandler?: Function): this {
1096+
const result = super.on(eventName, eventHandler);
1097+
1098+
const hasItemSwipeHandler = eventName === 'itemSwipe'
1099+
|| (isPlainObject(eventName) && Object.prototype.hasOwnProperty.call(eventName, 'itemSwipe'));
1100+
1101+
if (hasItemSwipeHandler) {
1102+
this._updateSwipeEventSubscription();
1103+
}
1104+
1105+
return result;
1106+
}
1107+
1108+
off(eventName: string, eventHandler?: Function): this {
1109+
const result = super.off(eventName, eventHandler);
1110+
1111+
if (eventName === 'itemSwipe') {
1112+
this._updateSwipeEventSubscription();
1113+
}
1114+
1115+
return result;
1116+
}
1117+
11051118
_nextButtonHandler(): void {
11061119
const pageLoadingArgs = {
11071120
component: this as unknown as dxList,
@@ -1408,7 +1421,6 @@ export class ListBase extends CollectionWidget<ListBaseProperties, Item> {
14081421
case 'badgeExpr':
14091422
this._invalidate();
14101423
break;
1411-
case '_swipeEnabled':
14121424
case '_onItemsRendered':
14131425
case 'selectByClick':
14141426
break;

packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/desktopTooltip.tests.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,13 +116,12 @@ QUnit.test('contentTemplate passed to createComponent should work correct', asyn
116116

117117
assert.equal(stubCreateComponent.getCall(1).args[0][0].nodeName, 'DIV');
118118
assert.equal(stubCreateComponent.getCall(1).args[1], List);
119-
assert.equal(Object.keys(stubCreateComponent.getCall(1).args[2]).length, 8);
119+
assert.equal(Object.keys(stubCreateComponent.getCall(1).args[2]).length, 7);
120120
assert.equal(stubCreateComponent.getCall(1).args[2].dataSource, dataList);
121121
assert.equal(stubCreateComponent.getCall(1).args[2].showScrollbar, 'onHover');
122122
assert.ok(stubCreateComponent.getCall(1).args[2].onContentReady);
123123
assert.ok(stubCreateComponent.getCall(1).args[2].onItemClick);
124124
assert.ok(stubCreateComponent.getCall(1).args[2].itemTemplate);
125-
assert.notOk(stubCreateComponent.getCall(1).args[2]._swipeEnabled);
126125
assert.equal(stubCreateComponent.getCall(1).args[2].pageLoadMode, 'scrollBottom'); // T1062566
127126
} finally {
128127
support.touch = _touch;

packages/devextreme/testing/tests/DevExpress.ui.widgets/listParts/commonTests.js

Lines changed: 90 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import eventsEngine from 'common/core/events/core/events_engine';
2424
import ariaAccessibilityTestHelper from '../../../helpers/ariaAccessibilityTestHelper.js';
2525

2626
const LIST_ITEM_CLASS = 'dx-list-item';
27+
const LIST_ITEM_CONTENT_CLASS = 'dx-list-item-content';
2728
const LIST_ITEMS_CLASS = 'dx-list-items';
2829
const LIST_GROUP_CLASS = 'dx-list-group';
2930
const LIST_GROUP_HEADER_CLASS = 'dx-list-group-header';
@@ -1139,29 +1140,6 @@ QUnit.module('options changed', moduleSetup, () => {
11391140
swipeItem();
11401141
});
11411142

1142-
QUnit.test('onItemSwipe handler should not be triggered if "_swipeEnabled" is false on init', function(assert) {
1143-
assert.expect(0);
1144-
1145-
const swipeHandler = () => {
1146-
assert.ok(true, 'swipe handled');
1147-
};
1148-
1149-
this.element.dxList({
1150-
items: [0],
1151-
onItemSwipe: swipeHandler,
1152-
_swipeEnabled: false
1153-
}).dxList('instance');
1154-
1155-
const item = $.proxy(function() {
1156-
return this.element.find(`.${LIST_ITEM_CLASS}`).eq(0);
1157-
}, this);
1158-
const swipeItem = () => {
1159-
pointerMock(item()).start().swipeStart().swipe(0.5).swipeEnd(1);
1160-
};
1161-
1162-
swipeItem();
1163-
});
1164-
11651143
QUnit.test('onItemSwipe - subscription by on method', function(assert) {
11661144
assert.expect(2);
11671145

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

1191-
list.on('itemSwipe', swipeHandler);
1169+
list.on({ 'itemSwipe': swipeHandler });
11921170
swipeItem();
11931171
});
11941172

@@ -4792,6 +4770,94 @@ QUnit.module('Search', () => {
47924770
});
47934771
});
47944772

4773+
QUnit.module('Highlighting/selecting', { ...moduleSetup, afterEach: function() {
4774+
moduleSetup.afterEach.call(this);
4775+
window.getSelection().removeAllRanges();
4776+
} }, () => {
4777+
4778+
const selectTextNodePart = (textNode, startOffset, endOffset) => {
4779+
const selection = window.getSelection();
4780+
const range = document.createRange();
4781+
selection.removeAllRanges();
4782+
range.setStart(textNode, startOffset);
4783+
range.setEnd(textNode, endOffset);
4784+
selection.addRange(range);
4785+
return selection;
4786+
};
4787+
4788+
const getFirstListItemAndTextNode = ($list) => {
4789+
const $item = $list.find(`.${LIST_ITEM_CLASS}`).eq(0);
4790+
const textNode = $item.find(`.${LIST_ITEM_CONTENT_CLASS}`).eq(0).get(0).firstChild;
4791+
4792+
return { $item, textNode };
4793+
};
4794+
4795+
QUnit.test('text selection should not be cleared when dragging on list item without onItemSwipe', function(assert) {
4796+
this.element.dxList({
4797+
items: ['Item 1', 'Item 2'],
4798+
});
4799+
4800+
const { $item, textNode } = getFirstListItemAndTextNode(this.element);
4801+
assert.strictEqual(!!textNode, true, 'text node found in list item');
4802+
4803+
selectTextNodePart(textNode, 0, 4);
4804+
assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag');
4805+
4806+
pointerMock($item).start().down(0, 0).move(50, 0).up();
4807+
4808+
assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists after drag');
4809+
});
4810+
4811+
QUnit.test('text selection should be preserved after onItemSwipe handler is removed from options', function(assert) {
4812+
this.element.dxList({
4813+
items: ['Item 1', 'Item 2'],
4814+
onItemSwipe: sinon.spy(),
4815+
});
4816+
const list = this.element.dxList('instance');
4817+
4818+
list.option('onItemSwipe', null);
4819+
4820+
const { $item, textNode } = getFirstListItemAndTextNode(this.element);
4821+
assert.strictEqual(!!textNode, true, 'text node found in list item');
4822+
4823+
selectTextNodePart(textNode, 0, 4);
4824+
assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag');
4825+
4826+
pointerMock($item).start().down(0, 0).move(50, 0).up();
4827+
4828+
assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists after drag');
4829+
});
4830+
4831+
QUnit.test('text selection should reflect itemSwipe on/off subscription state', function(assert) {
4832+
this.element.dxList({
4833+
items: ['Item 1', 'Item 2'],
4834+
});
4835+
4836+
const list = this.element.dxList('instance');
4837+
4838+
const { $item, textNode } = getFirstListItemAndTextNode(this.element);
4839+
assert.strictEqual(!!textNode, true, 'text node found in list item');
4840+
4841+
list.on('itemSwipe', sinon.spy());
4842+
4843+
selectTextNodePart(textNode, 0, 4);
4844+
assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag with subscribed swipe handler');
4845+
4846+
pointerMock($item).start().down(0, 0).move(50, 0).up();
4847+
4848+
assert.strictEqual(window.getSelection().toString(), '', 'text selection is cleared while swipe handler is attached');
4849+
4850+
list.off('itemSwipe');
4851+
4852+
selectTextNodePart(textNode, 0, 4);
4853+
assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists before drag');
4854+
4855+
pointerMock($item).start().down(0, 0).move(50, 0).up();
4856+
4857+
assert.strictEqual(window.getSelection().toString(), textNode.nodeValue.slice(0, 4), 'text selection exists after drag when swipe handler is removed');
4858+
});
4859+
});
4860+
47954861
let helper;
47964862
if(devices.real().deviceType === 'desktop') {
47974863
[true, false].forEach((searchEnabled) => {

0 commit comments

Comments
 (0)