Commit 6a1b2c94 authored by Kristi Park's avatar Kristi Park Committed by Commit Bot

[NTP] Add touch support for shortcut reordering

This is mostly the same as mouse support, except:
- 'touchstart' must be triggered first. Then, the subsequent 'touchmove'
  event can start the reorder flow.
- There is no touch equivalent for 'mouseover'. Hence, we emulate it
  by manually checking if we are hovering over another tile using
  using |document.elementsFromPoint|.

Screencast: https://drive.google.com/open?id=1DTbSzrcN9CBzvOWvziHPxz6w7_FIOPqJ

Bug: 851335
Change-Id: I2f91576c86c4d825c310612d4def14fa6fc227f2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1600597
Commit-Queue: Kristi Park <kristipark@chromium.org>
Reviewed-by: default avatarKyle Milka <kmilka@chromium.org>
Cr-Commit-Position: refs/heads/master@{#665010}
parent df758127
......@@ -317,6 +317,9 @@ class Grid {
this.itemToReorder_ = -1;
/** @private {number} The index to move the tile we're reordering to. */
this.newIndexOfItemToReorder_ = -1;
/** @private {boolean} True if the user is currently touching a tile. */
this.touchStarted_ = false;
}
......@@ -523,11 +526,13 @@ class Grid {
*/
setupReorder_(tile, index) {
tile.setAttribute('index', index);
// Set up mouse support.
// Listen for the drag event on the tile instead of the tile container. The
// tile container remains static during the reorder flow.
tile.firstChild.draggable = true;
tile.firstChild.addEventListener('dragstart', (event) => {
this.startReorder_(tile, event);
this.startReorder_(tile, event, /*mouseMode=*/ true);
});
// Listen for the mouseover event on the tile container. If this is placed
// on the tile instead, it can be triggered while the tile is translated to
......@@ -535,6 +540,51 @@ class Grid {
tile.addEventListener('mouseover', (event) => {
this.reorderToIndex_(index);
});
// Set up touch support.
tile.firstChild.addEventListener('touchstart', (startEvent) => {
// Ignore subsequent touchstart events, which can be triggered if a
// different finger is placed on this tile.
if (this.touchStarted_) {
return;
}
this.touchStarted_ = true;
// Start the reorder flow once the user moves their finger.
const startReorder = (moveEvent) => {
// Use the cursor position from 'touchstart' as the starting location.
this.startReorder_(tile, startEvent, /*mouseMode=*/ false);
};
// Insert the held tile at the index we are hovering over.
const moveOver = (moveEvent) => {
// Touch events do not have a 'mouseover' equivalent, so we need to
// manually check if we are hovering over a tile.
// Note: The first item in |changedTouches| is the current position.
const x = moveEvent.changedTouches[0].pageX;
const y = moveEvent.changedTouches[0].pageY;
const elements = document.elementsFromPoint(x, y);
for (let i = 0; i < elements.length; i++) {
if (elements[i].classList.contains('grid-tile-container')) {
this.reorderToIndex_(Number(elements[i].getAttribute('index')));
break;
}
}
};
// Allow 'touchstart' events again when reordering stops/was never
// started.
const touchEnd = (endEvent) => {
tile.firstChild.removeEventListener('touchmove', startReorder);
tile.firstChild.removeEventListener('touchmove', moveOver);
tile.firstChild.removeEventListener('touchend', touchEnd);
tile.firstChild.removeEventListener('touchcancel', touchEnd);
this.touchStarted_ = false;
};
tile.firstChild.addEventListener('touchmove', startReorder, {once: true});
tile.firstChild.addEventListener('touchmove', moveOver);
tile.firstChild.addEventListener('touchend', touchEnd, {once: true});
tile.firstChild.addEventListener('touchcancel', touchEnd, {once: true});
});
}
......@@ -542,11 +592,12 @@ class Grid {
* Starts the reorder flow. Updates the visual style of the held tile to
* indicate that it is being moved and sets up the relevant event listeners.
* @param {!Element} tile Tile that is being moved.
* @param {!Event} event The 'dragstart' event. Used to obtain the current
* cursor position
* @param {!Event} event The 'dragstart'/'touchmove' event. Used to obtain the
* current cursor position
* @param {boolean} mouseMode True if the user is using a mouse.
* @private
*/
startReorder_(tile, event) {
startReorder_(tile, event, mouseMode) {
const index = Number(tile.getAttribute('index'));
this.itemToReorder_ = index;
......@@ -557,13 +608,31 @@ class Grid {
// Disable other hover/active styling for all tiles.
document.body.classList.add(CLASSES.REORDERING);
// Set up event listeners for the reorder flow.
const mouseMove = this.trackCursor_(tile, event.pageX, event.pageY);
document.addEventListener('mousemove', mouseMove);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', mouseMove);
this.stopReorder_(tile);
}, {once: true});
// Set up event listeners for the reorder flow. Listen for mouse events if
// |mouseMode|, touch events otherwise.
if (mouseMode) {
const mouseMove = this.trackCursor_(tile, event.pageX, event.pageY, true);
document.addEventListener('mousemove', mouseMove);
document.addEventListener('mouseup', () => {
document.removeEventListener('mousemove', mouseMove);
this.stopReorder_(tile);
}, {once: true});
} else {
// Track the cursor on subsequent 'touchmove' events (the first
// 'touchmove' event that starts the reorder flow is ignored).
const trackCursor = this.trackCursor_(
tile, event.changedTouches[0].pageX, event.changedTouches[0].pageY,
false);
const touchEnd = (event) => {
tile.firstChild.removeEventListener('touchmove', trackCursor);
tile.firstChild.removeEventListener('touchend', touchEnd);
tile.firstChild.removeEventListener('touchcancel', touchEnd);
this.stopReorder_(tile); // Stop the reorder flow.
};
tile.firstChild.addEventListener('touchmove', trackCursor);
tile.firstChild.addEventListener('touchend', touchEnd, {once: true});
tile.firstChild.addEventListener('touchcancel', touchEnd, {once: true});
}
}
......@@ -646,9 +715,10 @@ class Grid {
* @param {!Element} tile Tile that is being moved.
* @param {number} origCursorX Original x cursor position.
* @param {number} origCursorY Original y cursor position.
* @param {boolean} mouseMode True if the user is using a mouse.
* @private
*/
trackCursor_(tile, origCursorX, origCursorY) {
trackCursor_(tile, origCursorX, origCursorY, mouseMode) {
const index = Number(tile.getAttribute('index'));
// RTL positions align with the right side of the grid. Therefore, the x
// value must be recalculated to align with the left.
......@@ -668,9 +738,11 @@ class Grid {
const minY = 0 - origPosY;
return (event) => {
const currX = mouseMode ? event.pageX : event.changedTouches[0].pageX;
const currY = mouseMode ? event.pageY : event.changedTouches[0].pageY;
// Do not exceed the iframe borders.
const x = Math.max(Math.min(event.pageX - origCursorX, maxX), minX);
const y = Math.max(Math.min(event.pageY - origCursorY, maxY), minY);
const x = Math.max(Math.min(currX - origCursorX, maxX), minX);
const y = Math.max(Math.min(currY - origCursorY, maxY), minY);
tile.firstChild.style.transform = 'translate(' + x + 'px, ' + y + 'px)';
};
}
......
......@@ -423,6 +423,68 @@ test.mostVisited.testReorderStart = function() {
};
/**
* Tests if the tiles can be reordered using touch.
*/
test.mostVisited.testReorderStartTouch = function() {
const params = { // Used to override the default grid parameters.
tileHeight: 10,
tileWidth: 10,
tilesAlwaysVisible: 6,
maxTilesPerRow: 5,
maxTiles: 10,
enableReorder: true
};
// Create a grid with 1 tile and an add shortcut button.
let container = document.createElement('div');
$(test.mostVisited.MOST_VISITED).appendChild(container);
test.mostVisited.initGridWithAdd(container, params, 2);
const touchStart = new Event('touchstart');
touchStart.changedTouches = [{pageX: 0, pageY: 0}]; // Point to some spot.
const touchMove = new Event('touchmove');
touchMove.changedTouches = [{pageX: 0, pageY: 0}]; // Point to some spot.
const touchEnd = new Event('touchend');
// Test that we can reorder a tile.
const tile = document.getElementsByClassName(
test.mostVisited.CLASSES.GRID_TILE_CONTAINER)[0];
assertEquals('false', tile.getAttribute('add'));
assertEquals(0, Number(tile.getAttribute('rid')));
assertFalse(tile.classList.contains(test.mostVisited.CLASSES.GRID_REORDER));
assertFalse(
document.body.classList.contains(test.mostVisited.CLASSES.REORDERING));
// Start the reorder flow.
tile.firstChild.dispatchEvent(touchStart);
tile.firstChild.dispatchEvent(touchMove);
assertTrue(tile.classList.contains(test.mostVisited.CLASSES.GRID_REORDER));
assertTrue(
document.body.classList.contains(test.mostVisited.CLASSES.REORDERING));
// Stop the reorder flow.
tile.firstChild.dispatchEvent(touchEnd);
assertFalse(tile.classList.contains(test.mostVisited.CLASSES.GRID_REORDER));
assertFalse(
document.body.classList.contains(test.mostVisited.CLASSES.REORDERING));
// Try and fail to reorder the add button.
let addButton = container.children[1];
assertEquals('true', addButton.getAttribute('add'));
addButton.firstChild.dispatchEvent(touchStart);
addButton.firstChild.dispatchEvent(touchMove);
assertFalse(
addButton.classList.contains(test.mostVisited.CLASSES.GRID_REORDER));
assertFalse(
document.body.classList.contains(test.mostVisited.CLASSES.REORDERING));
};
/**
* Tests if the held tile properly follows the cursor.
*/
......@@ -615,6 +677,75 @@ test.mostVisited.testReorderFollowCursorRtl = function() {
};
/**
* Tests if the held tile properly follows the cursor using touch.
*/
test.mostVisited.testReorderFollowCursorTouch = function() {
// Set the window so that there's 10px padding around the grid.
window.innerHeight = 40;
window.innerWidth = 40;
const params = { // Used to override the default grid parameters.
tileHeight: 10,
tileWidth: 10,
maxTilesPerRow: 2,
maxTiles: 4,
enableReorder: true
};
// Create a grid with max rows.
let container = document.createElement('div');
$(test.mostVisited.MOST_VISITED).appendChild(container);
test.mostVisited.initGrid(container, params, 4);
const touchStart = new Event('touchstart');
const touchMove = new Event('touchmove');
const touchEnd = new Event('touchend');
const tiles = document.getElementsByClassName(
test.mostVisited.CLASSES.GRID_TILE_CONTAINER);
assertEquals(4, tiles.length);
// Start the reorder flow on the center of the first tile.
let tile = tiles[0];
touchStart.changedTouches = [{pageX: 15, pageY: 15}];
touchMove.changedTouches = [{pageX: 0, pageY: 0}]; // Point to some spot.
tile.firstChild.dispatchEvent(touchStart);
tile.firstChild.dispatchEvent(touchMove);
// No style should be applied to the tile yet.
assertEquals('', tile.firstChild.style.transform);
// Move finger 5px right and down. This should also move the tile 5px right
// and down.
touchMove.changedTouches = [{pageX: 20, pageY: 20}];
let expectedTransform = 'translate(5px, 5px)';
// The first 'touchmove' event only starts the reorder flow, so a subsequent
// move event is required.
tile.firstChild.dispatchEvent(touchMove);
assertEquals(expectedTransform, tile.firstChild.style.transform);
// Move finger beyond the top right corner. This should move the tile to the
// top right corner of the grid but not beyond it.
touchMove.changedTouches = [{pageX: 40, pageY: 0}];
expectedTransform = 'translate(10px, 0px)';
tile.firstChild.dispatchEvent(touchMove);
assertEquals(expectedTransform, tile.firstChild.style.transform);
// Move finger beyond the bottom left corner. This should move the tile to the
// bottom left corner of the grid but not beyond it.
touchMove.changedTouches = [{pageX: 0, pageY: 40}];
expectedTransform = 'translate(0px, 10px)';
tile.firstChild.dispatchEvent(touchMove);
assertEquals(expectedTransform, tile.firstChild.style.transform);
// Stop the reorder flow.
tile.firstChild.dispatchEvent(touchEnd);
};
/**
* Tests if the tiles are translated properly when reordering.
*/
......@@ -885,6 +1016,90 @@ test.mostVisited.testReorderInsertWithAddButton = function() {
};
/**
* Tests if the tiles are translated properly when reordering using touch.
*/
test.mostVisited.testReorderInsertTouch = function() {
// Set the window so that there's 10px padding around the grid.
window.innerHeight = 40;
window.innerWidth = 50;
const params = { // Used to override the default grid parameters.
tileHeight: 10,
tileWidth: 10,
maxTilesPerRow: 3,
maxTiles: 6,
enableReorder: true
};
// Override for testing.
let testElementsFromPoint = [];
document.elementsFromPoint = (x, y) => {
return testElementsFromPoint;
};
// Create a grid with uneven rows.
let container = document.createElement('div');
container.classList.add('test');
$(test.mostVisited.MOST_VISITED).appendChild(container);
test.mostVisited.initGrid(container, params, 5);
$('most-visited').appendChild(container);
const touchStart = new Event('touchstart');
const touchMove = new Event('touchmove');
const touchEnd = new Event('touchend');
const tiles = document.getElementsByClassName(
test.mostVisited.CLASSES.GRID_TILE_CONTAINER);
assertEquals(5, tiles.length);
// Start the reorder flow on the center of the first tile.
let tile = tiles[0];
touchStart.changedTouches = [{pageX: 15, pageY: 15}];
touchMove.changedTouches = [{pageX: 0, pageY: 0}]; // Point to some spot.
tile.firstChild.dispatchEvent(touchStart);
tile.firstChild.dispatchEvent(touchMove);
// Move over the second tile. This should shift tiles as if the held tile
// was inserted after.
let expectedLayout = [
'', 'translate(-10px, 0px)', 'translate(0px, 0px)', 'translate(0px, 0px)',
'translate(0px, 0px)'
];
testElementsFromPoint = [tiles[1]];
tile.firstChild.dispatchEvent(touchMove);
test.mostVisited.assertReorderInsert(container, expectedLayout, 0);
// Move over the first tile. This should shift tiles as if the held tile was
// inserted before.
expectedLayout = [
'', 'translate(0px, 0px)', 'translate(0px, 0px)', 'translate(0px, 0px)',
'translate(0px, 0px)'
];
testElementsFromPoint = [tiles[0]];
tile.firstChild.dispatchEvent(touchMove);
test.mostVisited.assertReorderInsert(container, expectedLayout, 0);
// Move over the last tile. This should shift tiles as if the held tile was
// inserted after.
expectedLayout = [
'', 'translate(-10px, 0px)', 'translate(-10px, 0px)',
'translate(15px, -10px)', 'translate(-10px, 0px)'
];
testElementsFromPoint = [tiles[4]];
tile.firstChild.dispatchEvent(touchMove);
test.mostVisited.assertReorderInsert(container, expectedLayout, 0);
// Stop the reorder flow.
tile.firstChild.dispatchEvent(touchEnd);
// Check that the correct values were sent to the EmbeddedSearchAPI.
assertEquals(0, test.mostVisited.reorderRid);
assertEquals(4, test.mostVisited.reorderNewIndex);
};
// ***************************** HELPER FUNCTIONS *****************************
// These are used by the tests above.
......
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