Commit c5bf0f26 authored by John Lee's avatar John Lee Committed by Commit Bot

WebUI Tab Strip: Move drag and drop handling to a separate file

This CLs organizes most of the drag and drop logic to its own
manager service, in preparation for more complex logic. This CL
does not change any implementation and only moves code around.

Follow up CLs will modify the drag interactions to only modify the
DOM and allow canceling drag sessions, with drop being the commit
to move tabs or tab groups. This will also prepare the manager to
be able to support dragging tabs from other windows into place.

Bug: 1048894
Change-Id: Ieb66ee859a09b972f178a8c50513aa2f284e08d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2036932Reviewed-by: default avatarDemetrios Papadopoulos <dpapad@chromium.org>
Commit-Queue: John Lee <johntlee@chromium.org>
Cr-Commit-Position: refs/heads/master@{#738668}
parent 0fd86f27
......@@ -11,6 +11,7 @@ js_type_check("closure_compile") {
":alert_indicator",
":alert_indicators",
":custom_element",
":drag_manager",
":tab",
":tab_group",
":tab_list",
......@@ -40,6 +41,16 @@ js_library("alert_indicators") {
js_library("custom_element") {
}
js_library("drag_manager") {
deps = [
":tab",
":tab_group",
":tabs_api_proxy",
"//ui/webui/resources/js:assert.m",
"//ui/webui/resources/js:load_time_data.m",
]
}
js_library("tabs_api_proxy") {
deps = [ "//ui/webui/resources/js:cr.m" ]
externs_list = [
......@@ -73,6 +84,7 @@ js_library("tab_group") {
js_library("tab_list") {
deps = [
":custom_element",
":drag_manager",
":tab",
":tab_group",
":tab_strip_embedder_proxy",
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './strings.m.js';
import {assert} from 'chrome://resources/js/assert.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {isTabElement, TabElement} from './tab.js';
import {isTabGroupElement, TabGroupElement} from './tab_group.js';
import {TabsApiProxy} from './tabs_api_proxy.js';
/**
* Gets the data type of tab IDs on DataTransfer objects in drag events. This
* is a function so that loadTimeData can get overridden by tests.
* @return {string}
*/
function getTabIdDataType() {
return loadTimeData.getString('tabIdDataType');
}
/** @return {string} */
function getGroupIdDataType() {
return loadTimeData.getString('tabGroupIdDataType');
}
/**
* @interface
*/
export class DragManagerDelegate {
/**
* @param {!TabElement} tabElement
* @return {number}
*/
getIndexOfTab(tabElement) {}
/** @param {!Element} element */
showDropPlaceholder(element) {}
}
/** @typedef {!DragManagerDelegate|!HTMLElement} */
let DragManagerDelegateElement;
export class DragManager {
/** @param {!DragManagerDelegateElement} delegate */
constructor(delegate) {
/** @private {!DragManagerDelegateElement} */
this.delegate_ = delegate;
/**
* The element currently being dragged.
* @type {?TabElement|?TabGroupElement}
*/
this.draggedItem_ = null;
/** @type {!Element} */
this.dropPlaceholder_ = document.createElement('div');
this.dropPlaceholder_.id = 'dropPlaceholder';
/** @private {!TabsApiProxy} */
this.tabsProxy_ = TabsApiProxy.getInstance();
}
/**
* @param {!DragEvent} event
* @param {number} windowId
*/
continueDrag(event, windowId) {
event.preventDefault();
if (!this.draggedItem_) {
this.delegate_.showDropPlaceholder(this.dropPlaceholder_);
return;
}
event.dataTransfer.dropEffect = 'move';
if (isTabGroupElement(this.draggedItem_)) {
this.continueDragWithGroupElement_(event);
} else if (isTabElement(this.draggedItem_)) {
this.continueDragWithTabElement_(event, windowId);
}
}
/**
* @param {!DragEvent} event
* @private
*/
continueDragWithGroupElement_(event) {
const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
if (composedPath.includes(assert(this.draggedItem_))) {
// Dragging over itself or a child of itself.
return;
}
const dragOverTabElement =
/** @type {!TabElement|undefined} */ (composedPath.find(isTabElement));
if (dragOverTabElement && !dragOverTabElement.tab.pinned) {
const dragOverIndex = this.delegate_.getIndexOfTab(dragOverTabElement);
this.tabsProxy_.moveGroup(
this.draggedItem_.dataset.groupId, dragOverIndex);
return;
}
const dragOverGroupElement = composedPath.find(isTabGroupElement);
if (dragOverGroupElement) {
const dragOverIndex = this.delegate_.getIndexOfTab(
/** @type {!TabElement} */ (dragOverGroupElement.firstElementChild));
this.tabsProxy_.moveGroup(
this.draggedItem_.dataset.groupId, dragOverIndex);
}
}
/**
* @param {!DragEvent} event
* @param {number} windowId
* @private
*/
continueDragWithTabElement_(event, windowId) {
const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
const dragOverTabElement =
/** @type {?TabElement} */ (composedPath.find(isTabElement));
if (dragOverTabElement &&
dragOverTabElement.tab.pinned !== this.draggedItem_.tab.pinned) {
// Can only drag between the same pinned states.
return;
}
const dragOverTabGroup =
/** @type {?TabGroupElement} */ (composedPath.find(isTabGroupElement));
if (dragOverTabGroup &&
dragOverTabGroup.dataset.groupId !== this.draggedItem_.tab.groupId) {
this.tabsProxy_.groupTab(
this.draggedItem_.tab.id, dragOverTabGroup.dataset.groupId);
return;
}
if (!dragOverTabGroup && this.draggedItem_.tab.groupId) {
this.tabsProxy_.ungroupTab(this.draggedItem_.tab.id);
return;
}
if (!dragOverTabElement) {
return;
}
const dragOverIndex = this.delegate_.getIndexOfTab(dragOverTabElement);
this.tabsProxy_.moveTab(this.draggedItem_.tab.id, windowId, dragOverIndex);
}
cancelDrag() {
this.dropPlaceholder_.remove();
}
/**
* @param {!DragEvent} event
* @param {number} windowId
*/
drop(event, windowId) {
if (this.draggedItem_) {
// If there is a valid dragged item, the drag originated from this TabList
// and is handled already by previous dragover events.
return;
}
this.dropPlaceholder_.remove();
if (event.dataTransfer.types.includes(getTabIdDataType())) {
const tabId = Number(event.dataTransfer.getData(getTabIdDataType()));
if (Number.isNaN(tabId)) {
// Invalid tab ID. Return silently.
return;
}
this.tabsProxy_.moveTab(tabId, windowId, -1);
} else if (event.dataTransfer.types.includes(getGroupIdDataType())) {
const groupId = event.dataTransfer.getData(getGroupIdDataType());
this.tabsProxy_.moveGroup(groupId, -1);
}
}
/** @param {!DragEvent} event */
startDrag(event) {
const draggedItem =
/** @type {!Array<!Element>} */ (event.composedPath()).find(item => {
return isTabElement(item) || isTabGroupElement(item);
});
if (!draggedItem) {
return;
}
this.draggedItem_ = /** @type {!TabElement} */ (draggedItem);
event.dataTransfer.effectAllowed = 'move';
const draggedItemRect = this.draggedItem_.getBoundingClientRect();
this.draggedItem_.setDragging(true);
event.dataTransfer.setDragImage(
this.draggedItem_.getDragImage(), event.clientX - draggedItemRect.left,
event.clientY - draggedItemRect.top);
if (isTabElement(draggedItem)) {
event.dataTransfer.setData(
getTabIdDataType(), this.draggedItem_.tab.id.toString());
} else if (isTabGroupElement(draggedItem)) {
event.dataTransfer.setData(
getGroupIdDataType(), this.draggedItem_.dataset.groupId);
}
}
/** @param {!DragEvent} event */
stopDrag(event) {
if (!this.draggedItem_) {
return;
}
this.draggedItem_.setDragging(false);
this.draggedItem_ = null;
}
}
......@@ -379,3 +379,11 @@ export class TabElement extends CustomElement {
}
customElements.define('tabstrip-tab', TabElement);
/**
* @param {!Element} element
* @return {boolean}
*/
export function isTabElement(element) {
return element.tagName === 'TABSTRIP-TAB';
}
......@@ -42,3 +42,11 @@ export class TabGroupElement extends CustomElement {
}
customElements.define('tabstrip-tab-group', TabGroupElement);
/**
* @param {!Element} element
* @return {boolean}
*/
export function isTabGroupElement(element) {
return element.tagName === 'TABSTRIP-TAB-GROUP';
}
......@@ -14,8 +14,9 @@ import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {isRTL} from 'chrome://resources/js/util.m.js';
import {CustomElement} from './custom_element.js';
import {DragManager, DragManagerDelegate} from './drag_manager.js';
import {TabElement} from './tab.js';
import {TabGroupElement} from './tab_group.js';
import {isTabGroupElement, TabGroupElement} from './tab_group.js';
import {TabStripEmbedderProxy} from './tab_strip_embedder_proxy.js';
import {tabStripOptions} from './tab_strip_options.js';
import {TabData, TabGroupVisualData, TabsApiProxy} from './tabs_api_proxy.js';
......@@ -36,20 +37,6 @@ export function setScrollAnimationEnabledForTesting(enabled) {
scrollAnimationEnabled = enabled;
}
/**
* Gets the data type of tab IDs on DataTransfer objects in drag events. This
* is a function so that loadTimeData can get overridden by tests.
* @return {string}
*/
function getTabIdDataType() {
return loadTimeData.getString('tabIdDataType');
}
/** @return {string} */
function getGroupIdDataType() {
return loadTimeData.getString('tabGroupIdDataType');
}
/**
* @enum {string}
*/
......@@ -60,22 +47,7 @@ const LayoutVariable = {
TAB_WIDTH: '--tabstrip-tab-thumbnail-width',
};
/**
* @param {!Element} element
* @return {boolean}
*/
function isTabElement(element) {
return element.tagName === 'TABSTRIP-TAB';
}
/**
* @param {!Element} element
* @return {boolean}
*/
function isTabGroupElement(element) {
return element.tagName === 'TABSTRIP-TAB-GROUP';
}
/** @implements {DragManagerDelegate} */
class TabListElement extends CustomElement {
static get template() {
return `{__html_template__}`;
......@@ -182,16 +154,6 @@ class TabListElement extends CustomElement {
this.addWebUIListener_(
'tab-thumbnail-updated', this.tabThumbnailUpdated_.bind(this));
this.addEventListener(
'dragstart', (e) => this.onDragStart_(/** @type {!DragEvent} */ (e)));
this.addEventListener(
'dragend', (e) => this.onDragEnd_(/** @type {!DragEvent} */ (e)));
this.addEventListener('dragleave', () => this.onDragLeave_());
this.addEventListener(
'dragover', (e) => this.onDragOver_(/** @type {!DragEvent} */ (e)));
this.addEventListener(
'drop', e => this.onDrop_(/** @type {!DragEvent} */ (e)));
document.addEventListener('contextmenu', this.contextMenuListener_);
document.addEventListener(
'visibilitychange', this.documentVisibilityChangeListener_);
......@@ -203,6 +165,20 @@ class TabListElement extends CustomElement {
this.tabsApi_.createNewTab();
});
const dragManager = new DragManager(this);
this.addEventListener(
'dragstart', e => dragManager.startDrag(/** @type {!DragEvent} */ (e)));
this.addEventListener(
'dragend', e => dragManager.stopDrag(/** @type {!DragEvent} */ (e)));
this.addEventListener('dragleave', () => dragManager.cancelDrag());
this.addEventListener(
'dragover',
e => dragManager.continueDrag(
/** @type {!DragEvent} */ (e), this.windowId_));
this.addEventListener(
'drop',
e => dragManager.drop(/** @type {!DragEvent} */ (e), this.windowId_));
if (loadTimeData.getBoolean('showDemoOptions')) {
this.$('#demoOptions').style.display = 'block';
......@@ -397,6 +373,14 @@ class TabListElement extends CustomElement {
return /** @type {?TabElement} */ (this.$('tabstrip-tab[active]'));
}
/**
* @param {!TabElement} tabElement
* @return {number}
*/
getIndexOfTab(tabElement) {
return Array.prototype.indexOf.call(this.$all('tabstrip-tab'), tabElement);
}
/**
* @param {!LayoutVariable} variable
* @return {number} in pixels
......@@ -492,164 +476,6 @@ class TabListElement extends CustomElement {
});
}
/**
* @param {!DragEvent} event
* @private
*/
onDragEnd_(event) {
if (!this.draggedItem_) {
return;
}
this.draggedItem_.setDragging(false);
this.draggedItem_ = undefined;
}
/** @private */
onDragLeave_() {
this.dropPlaceholder_.remove();
}
/**
* @param {!DragEvent} event
* @private
*/
onDragOver_(event) {
event.preventDefault();
if (!this.draggedItem_) {
this.unpinnedTabsElement_.appendChild(this.dropPlaceholder_);
this.animateScrollPosition_(this.dropPlaceholder_.offsetLeft);
return;
}
event.dataTransfer.dropEffect = 'move';
if (isTabGroupElement(this.draggedItem_)) {
this.onDragOverWithGroupElement_(event);
} else if (isTabElement(this.draggedItem_)) {
this.onDragOverWithTabElement_(event);
}
}
/**
* @param {!DragEvent} event
* @private
*/
onDragOverWithGroupElement_(event) {
const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
if (composedPath.includes(assert(this.draggedItem_))) {
// Dragging over itself or a child of itself.
return;
}
const allTabElements = Array.from(this.$all('tabstrip-tab'));
const dragOverTabElement = composedPath.find(isTabElement);
if (dragOverTabElement && !dragOverTabElement.tab.pinned) {
const dragOverIndex = allTabElements.indexOf(dragOverTabElement);
this.tabsApi_.moveGroup(this.draggedItem_.dataset.groupId, dragOverIndex);
return;
}
const dragOverGroupElement = composedPath.find(isTabGroupElement);
if (dragOverGroupElement) {
const dragOverIndex =
allTabElements.indexOf(dragOverGroupElement.firstElementChild);
this.tabsApi_.moveGroup(this.draggedItem_.dataset.groupId, dragOverIndex);
}
}
/**
* @param {!DragEvent} event
* @private
*/
onDragOverWithTabElement_(event) {
const composedPath = /** @type {!Array<!Element>} */ (event.composedPath());
const dragOverTabElement = composedPath.find(isTabElement);
if (dragOverTabElement &&
dragOverTabElement.tab.pinned !== this.draggedItem_.tab.pinned) {
// Can only drag between the same pinned states.
return;
}
const dragOverTabGroup = composedPath.find(isTabGroupElement);
if (dragOverTabGroup &&
dragOverTabGroup.dataset.groupId !== this.draggedItem_.tab.groupId) {
this.tabsApi_.groupTab(
this.draggedItem_.tab.id, dragOverTabGroup.dataset.groupId);
return;
}
if (!dragOverTabGroup && this.draggedItem_.tab.groupId) {
this.tabsApi_.ungroupTab(this.draggedItem_.tab.id);
return;
}
if (!dragOverTabElement) {
return;
}
const dragOverIndex =
Array.from(this.$all('tabstrip-tab')).indexOf(dragOverTabElement);
this.tabsApi_.moveTab(
this.draggedItem_.tab.id, this.windowId_, dragOverIndex);
}
/**
* @param {!DragEvent} event
* @private
*/
onDragStart_(event) {
const draggedItem =
/** @type {!Array<!Element>} */ (event.composedPath()).find(item => {
return isTabElement(item) || isTabGroupElement(item);
});
if (!draggedItem) {
return;
}
this.draggedItem_ = /** @type {!TabElement} */ (draggedItem);
event.dataTransfer.effectAllowed = 'move';
const draggedItemRect = this.draggedItem_.getBoundingClientRect();
this.draggedItem_.setDragging(true);
event.dataTransfer.setDragImage(
this.draggedItem_.getDragImage(), event.clientX - draggedItemRect.left,
event.clientY - draggedItemRect.top);
if (isTabElement(draggedItem)) {
event.dataTransfer.setData(
getTabIdDataType(), this.draggedItem_.tab.id.toString());
} else if (isTabGroupElement(draggedItem)) {
event.dataTransfer.setData(
getGroupIdDataType(), this.draggedItem_.dataset.groupId);
}
}
/**
* @param {!DragEvent} event
* @private
*/
onDrop_(event) {
if (this.draggedItem_) {
// If there is a valid dragged item, the drag originated from this TabList
// and is handled already by previous dragover events.
return;
}
this.dropPlaceholder_.remove();
if (event.dataTransfer.types.includes(getTabIdDataType())) {
const tabId = Number(event.dataTransfer.getData(getTabIdDataType()));
if (Number.isNaN(tabId)) {
// Invalid tab ID. Return silently.
return;
}
this.tabsApi_.moveTab(tabId, this.windowId_, -1);
} else if (event.dataTransfer.types.includes(getGroupIdDataType())) {
const groupId = event.dataTransfer.getData(getGroupIdDataType());
this.tabsApi_.moveGroup(groupId, -1);
}
}
/** @private */
onReceivedKeyboardFocus_() {
// FocusOutlineManager relies on the most recent event fired on the
......@@ -907,6 +733,12 @@ class TabListElement extends CustomElement {
this.animateScrollPosition_(scrollBy);
}
/** @param {!Element} element */
showDropPlaceholder(element) {
this.unpinnedTabsElement_.appendChild(element);
this.animateScrollPosition_(element.offsetLeft);
}
/**
* @param {number} tabId
* @param {string} imgData
......
......@@ -72,6 +72,11 @@
file="tab_swiper.js"
type="chrome_html"
compress="gzip"/>
<structure
name="IDR_TAB_STRIP_DRAG_MANAGER_JS"
file="drag_manager.js"
type="chrome_html"
compress="gzip"/>
</structures>
<includes>
......
......@@ -139,6 +139,7 @@ export class TabsApiProxy {
/**
* @param {number} tabId
* @param {number} windowId
* @param {number} newIndex
* @return {!Promise<!ExtensionsApiTab>}
*/
......
// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {DragManager} from 'chrome://tab-strip/drag_manager.js';
import {TabElement} from 'chrome://tab-strip/tab.js';
import {TabGroupElement} from 'chrome://tab-strip/tab_group.js';
import {TabsApiProxy} from 'chrome://tab-strip/tabs_api_proxy.js';
import {TestTabsApiProxy} from './test_tabs_api_proxy.js';
class MockDelegate extends HTMLElement {
constructor() {
super();
}
getIndexOfTab(tabElement) {
return Array.from(this.querySelectorAll('tabstrip-tab'))
.indexOf(tabElement);
}
showDropPlaceholder(element) {
this.appendChild(element);
}
}
customElements.define('mock-delegate', MockDelegate);
class MockDataTransfer extends DataTransfer {
constructor() {
super();
this.dragImageData = {
image: undefined,
offsetX: undefined,
offsetY: undefined,
};
this.dropEffect_ = 'none';
this.effectAllowed_ = 'none';
}
get dropEffect() {
return this.dropEffect_;
}
set dropEffect(effect) {
this.dropEffect_ = effect;
}
get effectAllowed() {
return this.effectAllowed_;
}
set effectAllowed(effect) {
this.effectAllowed_ = effect;
}
setDragImage(image, offsetX, offsetY) {
this.dragImageData.image = image;
this.dragImageData.offsetX = offsetX;
this.dragImageData.offsetY = offsetY;
}
}
suite('DragManager', () => {
let delegate;
let dragManager;
let testTabsApiProxy;
const tabs = [
{
active: true,
alertStates: [],
id: 0,
index: 0,
pinned: false,
title: 'Tab 1',
},
{
active: false,
alertStates: [],
id: 1,
index: 1,
pinned: false,
title: 'Tab 2',
},
];
const delegateWindowId = 1001;
const strings = {
tabIdDataType: 'application/tab-id',
};
/**
* @param {!TabElement} tabElement
* @param {string} groupId
* @return {!TabGroupElement}
*/
function groupTab(tabElement, groupId) {
const groupElement = document.createElement('tabstrip-tab-group');
groupElement.setAttribute('data-group-id', groupId);
delegate.replaceChild(groupElement, tabElement);
tabElement.tab = Object.assign({}, tabElement.tab, {groupId});
groupElement.appendChild(tabElement);
return groupElement;
}
setup(() => {
loadTimeData.overrideValues(strings);
testTabsApiProxy = new TestTabsApiProxy();
TabsApiProxy.instance_ = testTabsApiProxy;
delegate = new MockDelegate();
tabs.forEach(tab => {
const tabElement = document.createElement('tabstrip-tab');
tabElement.tab = tab;
delegate.appendChild(tabElement);
});
dragManager = new DragManager(delegate, delegateWindowId);
delegate.addEventListener('dragstart', e => dragManager.startDrag(e));
delegate.addEventListener('dragend', e => dragManager.stopDrag(e));
delegate.addEventListener('dragleave', () => dragManager.cancelDrag());
delegate.addEventListener(
'dragover', (e) => dragManager.continueDrag(e, delegateWindowId));
delegate.addEventListener(
'drop', (e) => dragManager.drop(e, delegateWindowId));
});
test('DragStartSetsDragImage', () => {
const draggedTab = delegate.children[0];
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
assertEquals(dragStartEvent.dataTransfer.effectAllowed, 'move');
assertEquals(
mockDataTransfer.dragImageData.image, draggedTab.getDragImage());
assertEquals(
mockDataTransfer.dragImageData.offsetX, 100 - draggedTab.offsetLeft);
assertEquals(
mockDataTransfer.dragImageData.offsetY, 150 - draggedTab.offsetTop);
});
test('DragOverMovesTabs', async () => {
const draggedIndex = 0;
const dragOverIndex = 1;
const draggedTab = delegate.children[draggedIndex];
const dragOverTab = delegate.children[dragOverIndex];
const mockDataTransfer = new MockDataTransfer();
// Dispatch a dragstart event to start the drag process
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
// Move the draggedTab over the 2nd tab
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverTab.dispatchEvent(dragOverEvent);
assertEquals(dragOverEvent.dataTransfer.dropEffect, 'move');
const [tabId, windowId, newIndex] =
await testTabsApiProxy.whenCalled('moveTab');
assertEquals(tabId, tabs[draggedIndex].id);
assertEquals(delegateWindowId, windowId);
assertEquals(newIndex, dragOverIndex);
});
test('DragTabOverTabGroup', async () => {
const tabElements = delegate.children;
// Group the first tab.
const dragOverTabGroup = groupTab(tabElements[0], 'group0');
// Start dragging the second tab.
const draggedTab = tabElements[1];
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
// Drag the second tab over the newly created tab group.
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverTabGroup.dispatchEvent(dragOverEvent);
const [tabId, groupId] = await testTabsApiProxy.whenCalled('groupTab');
assertEquals(draggedTab.tab.id, tabId);
assertEquals('group0', groupId);
});
test('DragTabOutOfTabGroup', async () => {
// Group the first tab.
const draggedTab = delegate.children[0];
groupTab(draggedTab, 'group0');
// Start dragging the first tab.
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
// Drag the first tab out.
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
delegate.dispatchEvent(dragOverEvent);
const [tabId] = await testTabsApiProxy.whenCalled('ungroupTab');
assertEquals(draggedTab.tab.id, tabId);
});
test('DragGroupOverTab', async () => {
const tabElements = delegate.children;
// Start dragging the group.
const draggedGroup = groupTab(tabElements[0], 'group0');
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedGroup.dispatchEvent(dragStartEvent);
// Drag the group over the second tab.
const dragOverTab = tabElements[1];
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverTab.dispatchEvent(dragOverEvent);
const [groupId, index] = await testTabsApiProxy.whenCalled('moveGroup');
assertEquals('group0', groupId);
assertEquals(1, index);
});
test('DragGroupOverGroup', async () => {
const tabElements = delegate.children;
// Group the first tab and second tab separately.
const draggedGroup = groupTab(tabElements[0], 'group0');
const dragOverGroup = groupTab(tabElements[1], 'group1');
// Start dragging the first group.
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedGroup.dispatchEvent(dragStartEvent);
// Drag the group over the second tab.
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverGroup.dispatchEvent(dragOverEvent);
const [groupId, index] = await testTabsApiProxy.whenCalled('moveGroup');
assertEquals('group0', groupId);
assertEquals(1, index);
});
test('DragTabIntoListFromOutside', () => {
const mockDataTransfer = new MockDataTransfer();
mockDataTransfer.setData(strings.tabIdDataType, '1000');
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
delegate.dispatchEvent(dragOverEvent);
assertTrue(delegate.lastElementChild.id === 'dropPlaceholder');
delegate.dispatchEvent(new DragEvent('drop', dragOverEvent));
assertEquals(null, delegate.querySelector('#dropPlaceholder'));
});
test('DropTabIntoList', async () => {
const droppedTabId = 9000;
const mockDataTransfer = new MockDataTransfer();
mockDataTransfer.setData(strings.tabIdDataType, droppedTabId);
const dropEvent = new DragEvent('drop', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
delegate.dispatchEvent(dropEvent);
const [tabId, windowId, index] =
await testTabsApiProxy.whenCalled('moveTab');
assertEquals(droppedTabId, tabId);
assertEquals(delegateWindowId, windowId);
assertEquals(-1, index);
});
});
......@@ -12,43 +12,6 @@ import {TabsApiProxy} from 'chrome://tab-strip/tabs_api_proxy.js';
import {TestTabStripEmbedderProxy} from './test_tab_strip_embedder_proxy.js';
import {TestTabsApiProxy} from './test_tabs_api_proxy.js';
class MockDataTransfer extends DataTransfer {
constructor() {
super();
this.dragImageData = {
image: undefined,
offsetX: undefined,
offsetY: undefined,
};
this.dropEffect_ = 'none';
this.effectAllowed_ = 'none';
}
get dropEffect() {
return this.dropEffect_;
}
set dropEffect(effect) {
this.dropEffect_ = effect;
}
get effectAllowed() {
return this.effectAllowed_;
}
set effectAllowed(effect) {
this.effectAllowed_ = effect;
}
setDragImage(image, offsetX, offsetY) {
this.dragImageData.image = image;
this.dragImageData.offsetX = offsetX;
this.dragImageData.offsetY = offsetY;
}
}
suite('TabList', () => {
let callbackRouter;
let optionsCalled;
......@@ -84,10 +47,6 @@ suite('TabList', () => {
];
const currentWindowId = 1000;
const strings = {
tabIdDataType: 'application/tab-id',
};
function pinTabAt(tab, index) {
const changeInfo = {index: index, pinned: true};
const updatedTab = Object.assign({}, tab, changeInfo);
......@@ -113,7 +72,6 @@ suite('TabList', () => {
}
setup(() => {
loadTimeData.overrideValues(strings);
document.body.innerHTML = '';
document.body.style.margin = 0;
......@@ -509,228 +467,6 @@ suite('TabList', () => {
assertEquals(tabAtIndex0.tab.id, tabToGroup.id);
});
test('dragstart sets a drag image offset by the event coordinates', () => {
// Drag and drop only works for pinned tabs
tabs.forEach(pinTabAt);
const draggedTab = getPinnedTabs()[0];
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
assertEquals(dragStartEvent.dataTransfer.effectAllowed, 'move');
assertEquals(
mockDataTransfer.dragImageData.image, draggedTab.getDragImage());
assertEquals(
mockDataTransfer.dragImageData.offsetX, 100 - draggedTab.offsetLeft);
assertEquals(
mockDataTransfer.dragImageData.offsetY, 150 - draggedTab.offsetTop);
});
test('dragover moves tabs', async () => {
// Drag and drop only works for pinned tabs
tabs.forEach(pinTabAt);
const draggedIndex = 0;
const dragOverIndex = 1;
const draggedTab = getPinnedTabs()[draggedIndex];
const dragOverTab = getPinnedTabs()[dragOverIndex];
const mockDataTransfer = new MockDataTransfer();
// Dispatch a dragstart event to start the drag process
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
// Move the draggedTab over the 2nd tab
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverTab.dispatchEvent(dragOverEvent);
assertEquals(dragOverEvent.dataTransfer.dropEffect, 'move');
const [tabId, windowId, newIndex] =
await testTabsApiProxy.whenCalled('moveTab');
assertEquals(tabId, tabs[draggedIndex].id);
assertEquals(currentWindowId, windowId);
assertEquals(newIndex, dragOverIndex);
});
test('DragTabOverTabGroup', async () => {
const tabElements = getUnpinnedTabs();
// Group the first tab.
webUIListenerCallback(
'tab-group-state-changed', tabElements[0].tab.id, 0, 'group0');
// Start dragging the second tab.
const draggedTab = tabElements[1];
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
// Drag the second tab over the newly created tab group.
const dragOverTabGroup = getTabGroups()[0];
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverTabGroup.dispatchEvent(dragOverEvent);
const [tabId, groupId] = await testTabsApiProxy.whenCalled('groupTab');
assertEquals(draggedTab.tab.id, tabId);
assertEquals('group0', groupId);
});
test('DragTabOutOfTabGroup', async () => {
const tabElements = getUnpinnedTabs();
// Group the first tab.
webUIListenerCallback(
'tab-group-state-changed', tabElements[0].tab.id, 0, 'group0');
// Start dragging the first tab.
const draggedTab = tabElements[0];
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedTab.dispatchEvent(dragStartEvent);
// Drag the first tab out.
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
tabList.dispatchEvent(dragOverEvent);
const [tabId] = await testTabsApiProxy.whenCalled('ungroupTab');
assertEquals(draggedTab.tab.id, tabId);
});
test('DragGroupOverTab', async () => {
const tabElements = getUnpinnedTabs();
// Group the first tab.
webUIListenerCallback(
'tab-group-state-changed', tabElements[0].tab.id, 0, 'group0');
// Start dragging the group.
const draggedGroup = getTabGroups()[0];
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedGroup.dispatchEvent(dragStartEvent);
// Drag the group over the second tab.
const dragOverTab = tabElements[1];
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverTab.dispatchEvent(dragOverEvent);
const [groupId, index] = await testTabsApiProxy.whenCalled('moveGroup');
assertEquals('group0', groupId);
assertEquals(1, index);
});
test('DragGroupOverGroup', async () => {
const tabElements = getUnpinnedTabs();
// Group the first tab and second tab separately.
webUIListenerCallback(
'tab-group-state-changed', tabElements[0].tab.id, 0, 'group0');
webUIListenerCallback(
'tab-group-state-changed', tabElements[1].tab.id, 1, 'group1');
// Start dragging the first group.
const draggedGroup = getTabGroups()[0];
const mockDataTransfer = new MockDataTransfer();
const dragStartEvent = new DragEvent('dragstart', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
draggedGroup.dispatchEvent(dragStartEvent);
// Drag the group over the second tab.
const dragOverGroup = getTabGroups()[1];
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
dragOverGroup.dispatchEvent(dragOverEvent);
const [groupId, index] = await testTabsApiProxy.whenCalled('moveGroup');
assertEquals('group0', groupId);
assertEquals(1, index);
});
test('DragTabIntoListFromOutside', () => {
const mockDataTransfer = new MockDataTransfer();
mockDataTransfer.setData(strings.tabIdDataType, '1000');
const dragOverEvent = new DragEvent('dragover', {
bubbles: true,
composed: true,
dataTransfer: mockDataTransfer,
});
tabList.dispatchEvent(dragOverEvent);
assertTrue(
tabList.$('#unpinnedTabs').lastElementChild.id === 'dropPlaceholder');
tabList.dispatchEvent(new DragEvent('drop', dragOverEvent));
assertEquals(null, tabList.$('dropPlaceholder'));
});
test('DropTabIntoList', async () => {
const droppedTabId = 9000;
const mockDataTransfer = new MockDataTransfer();
mockDataTransfer.setData(strings.tabIdDataType, droppedTabId);
const dropEvent = new DragEvent('drop', {
bubbles: true,
composed: true,
clientX: 100,
clientY: 150,
dataTransfer: mockDataTransfer,
});
tabList.dispatchEvent(dropEvent);
const [tabId, windowId, index] =
await testTabsApiProxy.whenCalled('moveTab');
assertEquals(droppedTabId, tabId);
assertEquals(currentWindowId, windowId);
assertEquals(-1, index);
});
test('tracks and untracks thumbnails based on viewport', async () => {
// Wait for slideIn animations to complete updating widths and reset
// resolvers to track new calls.
......
......@@ -85,3 +85,13 @@ var TabStripTabGroupTest = class extends TabStripBrowserTest {
TEST_F('TabStripTabGroupTest', 'All', function() {
mocha.run();
});
var TabStripDragManagerTest = class extends TabStripBrowserTest {
get browsePreload() {
return 'chrome://tab-strip/test_loader.html?module=tab_strip/drag_manager_test.js';
}
};
TEST_F('TabStripDragManagerTest', 'All', function() {
mocha.run();
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment