diff --git a/src/mapml/control/LayerControl.js b/src/mapml/control/LayerControl.js index c30f0fa0d..4fb40ca92 100644 --- a/src/mapml/control/LayerControl.js +++ b/src/mapml/control/LayerControl.js @@ -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) { diff --git a/src/mapml/layers/MapMLLayer.js b/src/mapml/layers/MapMLLayer.js index 8bed812ab..a7761dde2 100644 --- a/src/mapml/layers/MapMLLayer.js +++ b/src/mapml/layers/MapMLLayer.js @@ -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; }, @@ -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; @@ -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); diff --git a/test/e2e/core/touchDevice.test.js b/test/e2e/core/touchDevice.test.js new file mode 100644 index 000000000..18e812bf6 --- /dev/null +++ b/test/e2e/core/touchDevice.test.js @@ -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"); + }); +}) \ No newline at end of file