Skip to content

Commit 32a7a40

Browse files
authored
map-feature bug fixes (#841)
* remove map-feature bug * fix/update Event implementation * stopPropagation attempt * Finalize map-feature Event handling * revert index.html * Add test for stopPropagation * update observed attributes + add test for click method * remove non-standard event parameter for click,focus,blur methods * add support/link for keyup+keydown event * close popup on zoom to here link, closes #855
1 parent 3cad259 commit 32a7a40

File tree

5 files changed

+195
-84
lines changed

5 files changed

+195
-84
lines changed

src/map-feature.js

Lines changed: 57 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export class MapFeature extends HTMLElement {
22
static get observedAttributes() {
3-
return ['zoom', 'onfocus', 'onclick', 'onblur'];
3+
return ['zoom', 'min', 'max'];
44
}
55

66
get zoom() {
@@ -88,15 +88,6 @@ export class MapFeature extends HTMLElement {
8888
}
8989
break;
9090
}
91-
case 'onfocus':
92-
case 'onclick':
93-
case 'onblur':
94-
if (this._groupEl) {
95-
// "synchronize" the onevent properties (i.e. onfocus, onclick, onblur)
96-
// between the mapFeature and its associated <g> element
97-
this._groupEl[name] = this[name].bind(this._groupEl);
98-
break;
99-
}
10091
}
10192
}
10293

@@ -183,15 +174,15 @@ export class MapFeature extends HTMLElement {
183174
}
184175

185176
_addFeature() {
186-
let parentEl =
177+
this._parentEl =
187178
this.parentNode.nodeName.toUpperCase() === 'LAYER-' ||
188179
this.parentNode.nodeName.toUpperCase() === 'MAP-EXTENT'
189180
? this.parentNode
190181
: this.parentNode.host;
191182

192183
// arrow function is not hoisted, define before use
193184
var _attachedToMap = (e) => {
194-
if (!parentEl._layer._map) {
185+
if (!this._parentEl._layer._map) {
195186
// if the parent layer- el has not yet added to the map (i.e. not yet rendered), wait until it is added
196187
this._layer.once(
197188
'attached',
@@ -229,21 +220,21 @@ export class MapFeature extends HTMLElement {
229220
}
230221
};
231222

232-
if (!parentEl._layer) {
223+
if (!this._parentEl._layer) {
233224
// for custom projection cases, the MapMLLayer has not yet created and binded with the layer- at this point,
234225
// because the "createMap" event of mapml-viewer has not yet been dispatched, the map has not yet been created
235226
// the event will be dispatched after defineCustomProjection > projection setter
236227
// should wait until MapMLLayer is built
237228
let parentLayer =
238-
parentEl.nodeName.toUpperCase() === 'LAYER-'
239-
? parentEl
240-
: parentEl.parentElement || parentEl.parentNode.host;
229+
this._parentEl.nodeName.toUpperCase() === 'LAYER-'
230+
? this._parentEl
231+
: this._parentEl.parentElement || this._parentEl.parentNode.host;
241232
parentLayer.parentNode.addEventListener('createmap', (e) => {
242233
this._layer = parentLayer._layer;
243234
_attachedToMap();
244235
});
245236
} else {
246-
this._layer = parentEl._layer;
237+
this._layer = this._parentEl._layer;
247238
_attachedToMap();
248239
}
249240
}
@@ -274,28 +265,25 @@ export class MapFeature extends HTMLElement {
274265
}
275266

276267
_setUpEvents() {
277-
['click', 'focus', 'blur'].forEach((name) => {
278-
// onevent properties & onevent attributes
279-
if (this[`on${name}`] && typeof this[`on${name}`] === 'function') {
280-
this._groupEl[`on${name}`] = this[`on${name}`];
281-
}
282-
// handle event handlers set via addEventlistener
283-
// for HTMLElement
268+
['click', 'focus', 'blur', 'keyup', 'keydown'].forEach((name) => {
284269
// when <g> is clicked / focused / blurred
285270
// should dispatch the click / focus / blur event listener on **linked HTMLFeatureElements**
286271
this._groupEl.addEventListener(name, (e) => {
287-
// this === mapFeature as arrow function does not have their own "this" pointer
288-
// store onEvent handler of mapFeature if there is any to ensure that it will not be re-triggered when the cloned mouseevent is dispatched
289-
// so that only the event handlers set on HTMLFeatureElement via addEventListener method will be triggered
290-
const handler = this[`on${name}`]; // a deep copy, var handler will not change when this.onevent is set to null (i.e. store the onevent property)
291-
this[`on${name}`] = null;
292272
if (name === 'click') {
293273
// dispatch a cloned mouseevent to trigger the click event handlers set on HTMLFeatureElement
294-
this.dispatchEvent(new PointerEvent(name, { ...e }));
274+
let clickEv = new PointerEvent(name, { cancelable: true });
275+
clickEv.originalEvent = e;
276+
this.dispatchEvent(clickEv);
277+
} else if (name === 'keyup' || name === 'keydown') {
278+
let keyEv = new KeyboardEvent(name, { cancelable: true });
279+
keyEv.originalEvent = e;
280+
this.dispatchEvent(keyEv);
295281
} else {
296-
this.dispatchEvent(new FocusEvent(name, { ...e }));
282+
// dispatch a cloned focusevent to trigger the focus/blue event handlers set on HTMLFeatureElement
283+
let focusEv = new FocusEvent(name, { cancelable: true });
284+
focusEv.originalEvent = e;
285+
this.dispatchEvent(focusEv);
297286
}
298-
this[`on${name}`] = handler;
299287
});
300288
});
301289
}
@@ -324,9 +312,7 @@ export class MapFeature extends HTMLElement {
324312
return this._layer._mapmlvectors._getNativeVariables(content);
325313
} else if (content.nodeName.toUpperCase() === 'LAYER-') {
326314
// for inline features, read native zoom and cs from inline map-meta
327-
let zoomMeta = this.parentElement.querySelectorAll(
328-
'map-meta[name=zoom]'
329-
),
315+
let zoomMeta = this._parentEl.querySelectorAll('map-meta[name=zoom]'),
330316
zoomLength = zoomMeta?.length;
331317
nativeZoom = zoomLength
332318
? +zoomMeta[zoomLength - 1]
@@ -336,7 +322,7 @@ export class MapFeature extends HTMLElement {
336322
?.split('=')[1]
337323
: 0;
338324

339-
let csMeta = this.parentElement.querySelectorAll('map-meta[name=cs]'),
325+
let csMeta = this._parentEl.querySelectorAll('map-meta[name=cs]'),
340326
csLength = csMeta?.length;
341327
nativeCS = csLength
342328
? csMeta[csLength - 1].getAttribute('content')
@@ -567,68 +553,55 @@ export class MapFeature extends HTMLElement {
567553
}
568554

569555
// a method that simulates a click, or invoking the user-defined click event
570-
// event (optional): a MouseEvent object, can be passed as an argument of the user-defined click event handlers
571-
click(event) {
556+
click() {
572557
let g = this._groupEl,
573558
rect = g.getBoundingClientRect();
574-
if (!event) {
575-
event = new MouseEvent('click', {
576-
clientX: rect.x + rect.width / 2,
577-
clientY: rect.y + rect.height / 2,
578-
button: 0
579-
});
559+
let event = new MouseEvent('click', {
560+
clientX: rect.x + rect.width / 2,
561+
clientY: rect.y + rect.height / 2,
562+
button: 0
563+
});
564+
let properties = this.querySelector('map-properties');
565+
if (g.getAttribute('role') === 'link') {
566+
for (let path of g.children) {
567+
path.mousedown.call(this._featureGroup, event);
568+
path.mouseup.call(this._featureGroup, event);
569+
}
580570
}
581-
if (typeof this.onclick === 'function') {
582-
this.onclick.call(this._groupEl, event);
583-
return;
584-
} else {
585-
let properties = this.querySelector('map-properties');
586-
if (g.getAttribute('role') === 'link') {
587-
for (let path of g.children) {
588-
path.mousedown.call(this._featureGroup, event);
589-
path.mouseup.call(this._featureGroup, event);
571+
// dispatch click event for map-feature to allow events entered by 'addEventListener'
572+
let clickEv = new PointerEvent('click', { cancelable: true });
573+
clickEv.originalEvent = event;
574+
this.dispatchEvent(clickEv);
575+
// for custom projection, layer- element may disconnect and re-attach to the map after the click
576+
// so check whether map-feature element is still connected before any further operations
577+
if (properties && this.isConnected) {
578+
let featureGroup = this._featureGroup,
579+
shapes = featureGroup._layers;
580+
// close popup if the popup is currently open
581+
for (let id in shapes) {
582+
if (shapes[id].isPopupOpen()) {
583+
shapes[id].closePopup();
590584
}
591585
}
592-
// for custom projection, layer- element may disconnect and re-attach to the map after the click
593-
// so check whether map-feature element is still connected before any further operations
594-
if (properties && this.isConnected) {
595-
let featureGroup = this._featureGroup,
596-
shapes = featureGroup._layers;
597-
// close popup if the popup is currently open
598-
for (let id in shapes) {
599-
if (shapes[id].isPopupOpen()) {
600-
shapes[id].closePopup();
601-
}
602-
}
603-
if (featureGroup.isPopupOpen()) {
604-
featureGroup.closePopup();
605-
} else {
606-
featureGroup.openPopup();
607-
}
586+
if (featureGroup.isPopupOpen()) {
587+
featureGroup.closePopup();
588+
} else if (!clickEv.originalEvent.cancelBubble) {
589+
// If stopPropagation is not set on originalEvent by user
590+
featureGroup.openPopup();
608591
}
609592
}
610593
}
611594

612595
// a method that sets the current focus to the <g> element, or invoking the user-defined focus event
613-
// event (optional): a FocusEvent object, can be passed as an argument of the user-defined focus event handlers
614-
// options (optional): as options parameter for native HTMLelemnt
596+
// options (optional): as options parameter for native HTMLElement
615597
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus
616-
focus(event, options) {
617-
let g = this._groupEl;
618-
if (typeof this.onfocus === 'function') {
619-
this.onfocus.call(this._groupEl, event);
620-
return;
621-
} else {
622-
g.focus(options);
623-
}
598+
focus(options) {
599+
this._groupEl.focus(options);
624600
}
625601

626602
// a method that makes the <g> element lose focus, or invoking the user-defined blur event
627-
// event (optional): a FocusEvent object, can be passed as an argument of the user-defined blur event handlers
628-
blur(event) {
629-
if (typeof this.onblur === 'function') {
630-
this.onblur.call(this._groupEl, event);
631-
} else if (
603+
blur() {
604+
if (
632605
document.activeElement.shadowRoot?.activeElement === this._groupEl ||
633606
document.activeElement.shadowRoot?.activeElement.parentNode ===
634607
this._groupEl

src/mapml/layers/MapMLLayer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2042,6 +2042,7 @@ export var MapMLLayer = L.Layer.extend({
20422042
if (!(e instanceof MouseEvent) && e.keyCode !== 13) return;
20432043
e.preventDefault();
20442044
featureEl.zoomTo();
2045+
featureEl._map.closePopup();
20452046
};
20462047
content.insertBefore(
20472048
zoomLink,

test/e2e/core/mapFeature.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,3 +292,63 @@ test.describe('Playwright MapFeature Custom Element Tests', () => {
292292
expect(test).toEqual(true);
293293
});
294294
});
295+
296+
test.describe('MapFeature Events', () => {
297+
let page, context;
298+
test.beforeAll(async () => {
299+
context = await chromium.launchPersistentContext('');
300+
page =
301+
context.pages().find((page) => page.url() === 'about:blank') ||
302+
(await context.newPage());
303+
await page.goto('mapFeature1.html');
304+
});
305+
test.afterAll(async function () {
306+
await context.close();
307+
});
308+
309+
test('Custom Click event - stopPropagation', async () => {
310+
// Click on polygon
311+
await page
312+
.locator(
313+
'mapml-viewer[role="application"]:has-text("Polygon -75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978 -75")'
314+
)
315+
.click();
316+
const popupCount = await page.$eval(
317+
'body > mapml-viewer > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane',
318+
(popupPane) => popupPane.childElementCount
319+
);
320+
// expect no popup is binded
321+
expect(popupCount).toEqual(0);
322+
323+
// custom click property displaying on div
324+
const propertyDiv = await page.$eval(
325+
'body > div#property',
326+
(div) => div.firstElementChild.innerText
327+
);
328+
// check custom event is displaying properties
329+
expect(propertyDiv).toEqual('This is a Polygon');
330+
});
331+
332+
test('click() method - stopPropagation', async () => {
333+
// click() method on line feature
334+
await page.$eval(
335+
'body > mapml-viewer > layer- > map-feature#line',
336+
(line) => line.click()
337+
);
338+
339+
const popupCount = await page.$eval(
340+
'body > mapml-viewer > div > div.leaflet-pane.leaflet-map-pane > div.leaflet-pane.leaflet-popup-pane',
341+
(popupPane) => popupPane.childElementCount
342+
);
343+
// expect no popup is binded
344+
expect(popupCount).toEqual(0);
345+
346+
// custom click property displaying on div
347+
const propertyDiv = await page.$eval(
348+
'body > div#property',
349+
(div) => div.firstElementChild.innerText
350+
);
351+
// check custom event is displaying properties
352+
expect(propertyDiv).toEqual('This is a Line');
353+
});
354+
});

test/e2e/core/mapFeature1.html

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
4+
<head>
5+
<title>map-feature Event tests</title>
6+
<meta charset="UTF-8">
7+
<script type="module" src="mapml-viewer.js"></script>
8+
</head>
9+
10+
<body>
11+
<mapml-viewer width="500" height="500" projection="OSMTILE" zoom="10" lon="-75.7" lat="45.4" controls>
12+
<layer- label="Features" checked>
13+
<map-meta name="projection" content="OSMTILE"></map-meta>
14+
<map-feature>
15+
<map-featurecaption>Polygon</map-featurecaption>
16+
<map-geometry cs="gcrs">
17+
<map-polygon class="polygon">
18+
<map-coordinates>-75.5859375 45.4656690 -75.6813812 45.4533876 -75.6961441 45.4239978
19+
-75.7249832 45.4083331 -75.7792282 45.3772317 -75.7534790 45.3294614 -75.5831909 45.3815724
20+
-75.6024170 45.4273712 -75.5673981 45.4639834 -75.5859375 45.4656690</map-coordinates>
21+
</map-polygon>
22+
</map-geometry>
23+
<map-properties>
24+
<h2>This is a Polygon</h2>
25+
</map-properties>
26+
</map-feature>
27+
28+
<map-feature id="line">
29+
<map-featurecaption>Line</map-featurecaption>
30+
<map-geometry cs="gcrs">
31+
<map-linestring class="line">
32+
<map-coordinates>-75.6168365 45.471929 -75.6855011 45.458445 -75.7016373 45.4391764 -75.7030106
33+
45.4259255 -75.7236099 45.4208652 -75.7565689 45.4117074 -75.7833481 45.384225 -75.8197403
34+
45.3714435 -75.8516693 45.377714</map-coordinates>
35+
</map-linestring>
36+
</map-geometry>
37+
<map-properties>
38+
<h2>This is a Line</h2>
39+
</map-properties>
40+
</map-feature>
41+
42+
<map-feature>
43+
<map-featurecaption>Point</map-featurecaption>
44+
<map-geometry cs="gcrs">
45+
<map-point class="point">
46+
<map-coordinates>-75.6916809 45.4186964</map-coordinates>
47+
</map-point>
48+
</map-geometry>
49+
<map-properties>
50+
<h2>This is a Point</h2>
51+
</map-properties>
52+
</map-feature>
53+
</layer->
54+
</mapml-viewer>
55+
56+
<div id="property"></div>
57+
58+
<script>
59+
function preventDefaultFunc(e) {
60+
e.originalEvent.stopPropagation();
61+
let out = document.getElementById('property');
62+
out.innerHTML = e.srcElement.querySelector("map-properties").innerHTML;
63+
}
64+
// Replace map-feature click handling
65+
document.querySelectorAll('map-feature').forEach((f) => {
66+
f.onclick = preventDefaultFunc;
67+
});
68+
</script>
69+
</body>
70+
71+
</html>

test/e2e/layers/queryLink.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,12 @@ test.describe('Playwright Query Link Tests', () => {
304304
await page.keyboard.press('Enter');
305305
await page.waitForTimeout(200);
306306

307+
// zoom to here link closes popup
308+
const popupCount = await page.evaluate(
309+
`document.querySelector("mapml-viewer").shadowRoot.querySelector(".leaflet-popup-pane").childElementCount`
310+
);
311+
expect(popupCount).toBe(0);
312+
307313
const endTopLeft = await page.evaluate(
308314
`document.querySelector('mapml-viewer').extent.topLeft.pcrs`
309315
);

0 commit comments

Comments
 (0)