Skip to content

Commit

Permalink
Layer control on touch device (#771)
Browse files Browse the repository at this point in the history
* reorder layers, prevent opacity setting showing up

* small changes

* create a test for touch devices

* test for touch devices

* small changes in test

* format and small edits

* continue formatting
  • Loading branch information
yhy0217 authored Mar 2, 2023
1 parent d89e27b commit e171be8
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 133 deletions.
23 changes: 16 additions & 7 deletions src/mapml/control/LayerControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,16 +146,25 @@ export var LayerControl = L.Control.Layers.extend({
) return this;

L.DomUtil.removeClass(this._container, 'leaflet-control-layers-expanded');
if (e.originalEvent?.pointerType === 'touch') {
this._container._isExpanded = false;
}
return this;
},
_preventDefaultContextMenu: function (e) {
let latlng = this._map.mouseEventToLatLng(e);
let containerPoint = this._map.mouseEventToContainerPoint(e);
e.preventDefault();
this._map.fire('contextmenu',
{ originalEvent: e,
containerPoint: containerPoint,
latlng: latlng });
let latlng = this._map.mouseEventToLatLng(e);
let containerPoint = this._map.mouseEventToContainerPoint(e);
e.preventDefault();
// for touch devices, when the layer control is not expanded,
// the layer context menu should not show on map
if (!this._container._isExpanded && e.pointerType === 'touch') {
this._container._isExpanded = true;
return;
}
this._map.fire('contextmenu',
{ originalEvent: e,
containerPoint: containerPoint,
latlng: latlng });
}
});
export var layerControl = function (layers, options) {
Expand Down
262 changes: 136 additions & 126 deletions src/mapml/layers/MapMLLayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -499,75 +499,76 @@ export var MapMLLayer = L.Layer.extend({
extent.setAttribute('aria-labelledby', extentItemNameSpan.id);
extentItemNameSpan.extent = this._extent._mapExtents[i];

extent.onmousedown = (downEvent) => {
if(downEvent.target.tagName.toLowerCase() === "input" || downEvent.target.tagName.toLowerCase() === "select") return;
downEvent.preventDefault();
downEvent.stopPropagation();

let control = extent,
controls = extent.parentNode,
moving = false, yPos = downEvent.clientY;

document.body.onmousemove = (moveEvent) => {
moveEvent.preventDefault();

// Fixes flickering by only moving element when there is enough space
let offset = moveEvent.clientY - yPos;
moving = Math.abs(offset) > 5 || moving;
if( (controls && !moving) || (controls && controls.childElementCount <= 1) ||
controls.getBoundingClientRect().top > control.getBoundingClientRect().bottom ||
controls.getBoundingClientRect().bottom < control.getBoundingClientRect().top){
return;
}

controls.classList.add("mapml-draggable");
control.style.transform = "translateY("+ offset +"px)";
control.style.pointerEvents = "none";

let x = moveEvent.clientX, y = moveEvent.clientY,
root = mapEl.tagName === "MAPML-VIEWER" ? mapEl.shadowRoot : mapEl.querySelector(".mapml-web-map").shadowRoot,
elementAt = root.elementFromPoint(x, y),
swapControl = !elementAt || !elementAt.closest("fieldset") ? control : elementAt.closest("fieldset");

swapControl = Math.abs(offset) <= swapControl.offsetHeight ? control : swapControl;

control.setAttribute("aria-grabbed", 'true');
control.setAttribute("aria-dropeffect", "move");
if(swapControl && controls === swapControl.parentNode){
swapControl = swapControl !== control.nextSibling? swapControl : swapControl.nextSibling;
if(control !== swapControl){
yPos = moveEvent.clientY;
control.style.transform = null;
}
controls.insertBefore(control, swapControl);
}
};

document.body.onmouseup = () => {
control.setAttribute("aria-grabbed", "false");
control.removeAttribute("aria-dropeffect");
control.style.pointerEvents = null;
control.style.transform = null;
let controlsElems = controls.children,
zIndex = 0;
for(let c of controlsElems){
let extentEl = c.querySelector("span").extent;

extentEl.setAttribute("data-moving","");
layerEl.insertAdjacentElement("beforeend", extentEl);
extentEl.removeAttribute("data-moving");

extentEl.extentZIndex = zIndex;
extentEl.templatedLayer.setZIndex(zIndex);
zIndex++;
extent.ontouchstart = extent.onmousedown = (downEvent) => {
if((downEvent.target.parentElement.tagName.toLowerCase() === 'label' &&
downEvent.target.tagName.toLowerCase() !== 'input') ||
downEvent.target.tagName.toLowerCase() === 'label') {
downEvent.stopPropagation();
downEvent = downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent;

let control = extent,
controls = extent.parentNode,
moving = false, yPos = downEvent.clientY;

document.body.ontouchmove = document.body.onmousemove = (moveEvent) => {
moveEvent.preventDefault();
moveEvent = moveEvent instanceof TouchEvent ? moveEvent.touches[0] : moveEvent;

// Fixes flickering by only moving element when there is enough space
let offset = moveEvent.clientY - yPos;
moving = Math.abs(offset) > 5 || moving;
if( (controls && !moving) || (controls && controls.childElementCount <= 1) ||
controls.getBoundingClientRect().top > control.getBoundingClientRect().bottom ||
controls.getBoundingClientRect().bottom < control.getBoundingClientRect().top){
return;
}

controls.classList.add("mapml-draggable");
control.style.transform = "translateY("+ offset +"px)";
control.style.pointerEvents = "none";

let x = moveEvent.clientX, y = moveEvent.clientY,
root = mapEl.tagName === "MAPML-VIEWER" ? mapEl.shadowRoot : mapEl.querySelector(".mapml-web-map").shadowRoot,
elementAt = root.elementFromPoint(x, y),
swapControl = !elementAt || !elementAt.closest("fieldset") ? control : elementAt.closest("fieldset");

swapControl = Math.abs(offset) <= swapControl.offsetHeight ? control : swapControl;

control.setAttribute("aria-grabbed", 'true');
control.setAttribute("aria-dropeffect", "move");
if(swapControl && controls === swapControl.parentNode){
swapControl = swapControl !== control.nextSibling? swapControl : swapControl.nextSibling;
if(control !== swapControl){
yPos = moveEvent.clientY;
control.style.transform = null;
}
controls.classList.remove("mapml-draggable");
document.body.onmousemove = document.body.onmouseup = null;
};
controls.insertBefore(control, swapControl);
}
};


document.body.ontouchend = document.body.onmouseup = () => {
control.setAttribute("aria-grabbed", "false");
control.removeAttribute("aria-dropeffect");
control.style.pointerEvents = null;
control.style.transform = null;
let controlsElems = controls.children,
zIndex = 0;
for(let c of controlsElems){
let extentEl = c.querySelector("span").extent;

extentEl.setAttribute("data-moving","");
layerEl.insertAdjacentElement("beforeend", extentEl);
extentEl.removeAttribute("data-moving");

extentEl.extentZIndex = zIndex;
extentEl.templatedLayer.setZIndex(zIndex);
zIndex++;
}
controls.classList.remove("mapml-draggable");
document.body.ontouchmove = document.body.onmousemove = document.body.ontouchend = document.body.onmouseup = null;
};
}
};

return extent;
},

Expand Down Expand Up @@ -635,6 +636,11 @@ export var MapMLLayer = L.Layer.extend({
itemSettingControlButton.setAttribute('aria-expanded', false);
itemSettingControlButton.classList.add('mapml-button');
L.DomEvent.on(itemSettingControlButton, 'click', (e)=>{
let layerControl = this._layerEl._layerControl._container;
if(!layerControl._isExpanded && e.pointerType === 'touch') {
layerControl._isExpanded = true;
return;
}
if(layerItemSettings.hidden === true){
itemSettingControlButton.setAttribute('aria-expanded', true);
layerItemSettings.hidden = false;
Expand Down Expand Up @@ -675,69 +681,73 @@ export var MapMLLayer = L.Layer.extend({
fieldset.setAttribute("aria-grabbed", "false");
fieldset.setAttribute('aria-labelledby', layerItemName.id);

fieldset.onmousedown = (downEvent) => {
if(downEvent.target.tagName.toLowerCase() === "input" || downEvent.target.tagName.toLowerCase() === "select") return;
downEvent.preventDefault();
let control = fieldset,
controls = fieldset.parentNode,
moving = false, yPos = downEvent.clientY;

document.body.onmousemove = (moveEvent) => {
moveEvent.preventDefault();

// Fixes flickering by only moving element when there is enough space
let offset = moveEvent.clientY - yPos;
moving = Math.abs(offset) > 5 || moving;
if( (controls && !moving) || (controls && controls.childElementCount <= 1) ||
controls.getBoundingClientRect().top > control.getBoundingClientRect().bottom ||
controls.getBoundingClientRect().bottom < control.getBoundingClientRect().top){
return;
}

controls.classList.add("mapml-draggable");
control.style.transform = "translateY("+ offset +"px)";
control.style.pointerEvents = "none";

let x = moveEvent.clientX, y = moveEvent.clientY,
root = mapEl.tagName === "MAPML-VIEWER" ? mapEl.shadowRoot : mapEl.querySelector(".mapml-web-map").shadowRoot,
elementAt = root.elementFromPoint(x, y),
swapControl = !elementAt || !elementAt.closest("fieldset") ? control : elementAt.closest("fieldset");

swapControl = Math.abs(offset) <= swapControl.offsetHeight ? control : swapControl;

control.setAttribute("aria-grabbed", 'true');
control.setAttribute("aria-dropeffect", "move");
if(swapControl && controls === swapControl.parentNode){
swapControl = swapControl !== control.nextSibling? swapControl : swapControl.nextSibling;
if(control !== swapControl){
yPos = moveEvent.clientY;
control.style.transform = null;
fieldset.ontouchstart = fieldset.onmousedown = (downEvent) => {
if((downEvent.target.parentElement.tagName.toLowerCase() === 'label' &&
downEvent.target.tagName.toLowerCase() !== 'input') ||
downEvent.target.tagName.toLowerCase() === 'label') {
downEvent = downEvent instanceof TouchEvent ? downEvent.touches[0] : downEvent;
let control = fieldset,
controls = fieldset.parentNode,
moving = false, yPos = downEvent.clientY;

document.body.ontouchmove = document.body.onmousemove = (moveEvent) => {
moveEvent.preventDefault();
moveEvent = moveEvent instanceof TouchEvent ? moveEvent.touches[0] : moveEvent;

// Fixes flickering by only moving element when there is enough space
let offset = moveEvent.clientY - yPos;
moving = Math.abs(offset) > 5 || moving;
if( (controls && !moving) || (controls && controls.childElementCount <= 1) ||
controls.getBoundingClientRect().top > control.getBoundingClientRect().bottom ||
controls.getBoundingClientRect().bottom < control.getBoundingClientRect().top){
return;
}
controls.insertBefore(control, swapControl);
}
};

document.body.onmouseup = () => {
control.setAttribute("aria-grabbed", "false");
control.removeAttribute("aria-dropeffect");
control.style.pointerEvents = null;
control.style.transform = null;
let controlsElems = controls.children,
zIndex = 1;
for(let c of controlsElems){
let layerEl = c.querySelector("span").layer._layerEl;

layerEl.setAttribute("data-moving","");
mapEl.insertAdjacentElement("beforeend", layerEl);
layerEl.removeAttribute("data-moving");

controls.classList.add("mapml-draggable");
control.style.transform = "translateY("+ offset +"px)";
control.style.pointerEvents = "none";

let x = moveEvent.clientX, y = moveEvent.clientY,
root = mapEl.tagName === "MAPML-VIEWER" ? mapEl.shadowRoot : mapEl.querySelector(".mapml-web-map").shadowRoot,
elementAt = root.elementFromPoint(x, y),
swapControl = !elementAt || !elementAt.closest("fieldset") ? control : elementAt.closest("fieldset");

swapControl = Math.abs(offset) <= swapControl.offsetHeight ? control : swapControl;

layerEl._layer.setZIndex(zIndex);
zIndex++;
}
controls.classList.remove("mapml-draggable");
document.body.onmousemove = document.body.onmouseup = null;
};
control.setAttribute("aria-grabbed", 'true');
control.setAttribute("aria-dropeffect", "move");
if(swapControl && controls === swapControl.parentNode){
swapControl = swapControl !== control.nextSibling? swapControl : swapControl.nextSibling;
if(control !== swapControl){
yPos = moveEvent.clientY;
control.style.transform = null;
}
controls.insertBefore(control, swapControl);
}
};

document.body.ontouchend = document.body.onmouseup = () => {
control.setAttribute("aria-grabbed", "false");
control.removeAttribute("aria-dropeffect");
control.style.pointerEvents = null;
control.style.transform = null;
let controlsElems = controls.children,
zIndex = 1;
for(let c of controlsElems){
let layerEl = c.querySelector("span").layer._layerEl;

layerEl.setAttribute("data-moving","");
mapEl.insertAdjacentElement("beforeend", layerEl);
layerEl.removeAttribute("data-moving");


layerEl._layer.setZIndex(zIndex);
zIndex++;
}
controls.classList.remove("mapml-draggable");
document.body.ontouchmove = document.body.onmousemove = document.body.onmouseup = null;
};
}
};

L.DomEvent.on(opacity,'change', this._changeOpacity, this);
Expand Down
54 changes: 54 additions & 0 deletions test/e2e/core/touchDevice.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { test, expect, chromium, devices } from '@playwright/test';
const device = devices['Pixel 5'];

test.describe("Playwright touch device tests", () => {
let page;
let context;
test.beforeAll(async () => {
// the test must be run in headless mode
// to successfully emulate a touch device with mouse disabled
context = await chromium.launch();
page = await context.newPage({
...device,
});
await page.goto("layerContextMenu.html");
});

test.afterAll(async function () {
await context.close();
});

test("Tap/Long press to show layer control", async () => {
const layerControl = await page.locator("div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div");
await layerControl.tap();
let className = await layerControl.evaluate(
(el) => el.classList.contains('leaflet-control-layers-expanded') && el._isExpanded
);
expect(className).toEqual(true);

// expect the opacity setting not open after the click
let opacity = await page.$eval(
"div > div.leaflet-control-container > div.leaflet-top.leaflet-right > div > section > div.leaflet-control-layers-overlays > fieldset:nth-child(1) > div.mapml-layer-item-properties > div > button:nth-child(2)",
(btn) => btn.getAttribute('aria-expanded')
);
expect(opacity).toEqual("false");

// long press
await page.tap('body > mapml-viewer');
await layerControl.dispatchEvent('touchstart');
await page.waitForTimeout(2000);
await layerControl.dispatchEvent('touchend');

className = await layerControl.evaluate(
(el) => el.classList.contains('leaflet-control-layers-expanded') && el._isExpanded
);
expect(className).toEqual(true);

// expect the layer context menu not show after the long press
const aHandle = await page.evaluateHandle(() => document.querySelector("mapml-viewer"));
const nextHandle = await page.evaluateHandle(doc => doc.shadowRoot, aHandle);
const resultHandle = await page.evaluateHandle(root => root.querySelector(".mapml-contextmenu.mapml-layer-menu"), nextHandle);
const menuDisplay = await (await page.evaluateHandle(elem => window.getComputedStyle(elem).getPropertyValue("display"), resultHandle)).jsonValue();
expect(menuDisplay).toEqual("none");
});
})

0 comments on commit e171be8

Please sign in to comment.