Skip to content

Commit 5dd8310

Browse files
Bug 1994100 - Add WPTs for mouse/pointer boundary events when usemap of <img> or coord of <area> is modified r=smaug,dom-core
It's unclear that how `<area>` is treated within CSS. However, it's treated as a transparent object at least when a hit-test of pointing device events and `<area>` can be a target of mouse events and pointer events. When the pointer is moved over the shape of an `<area>`, browsers dispatch `mouseover` and `pointerover` on the `<area>` instead of the `<img>`. Then, when it's moved out from the shape, browsers dispatch `mouseout` and `pointerout`. However, Chrome and Safari do not dispatch boundary events when the underneath element of the last pointer position becomes another element. This does not make sense because modifying the layout, e.g., modifying the `<img>` size, causes `mouseover` and `pointerover` on the new element anyway. Therefore, modifying `usemap` attribute of `<img>` and `coord` attribute of `<area>` should be treated as a layout change. Differential Revision: https://phabricator.services.mozilla.com/D268655
1 parent debdf7f commit 5dd8310

File tree

3 files changed

+760
-0
lines changed

3 files changed

+760
-0
lines changed

layout/generic/nsImageMap.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
#include "nsImageMap.h"
1010

11+
#include "mozilla/PresShell.h"
1112
#include "mozilla/UniquePtr.h"
1213
#include "mozilla/dom/Element.h"
1314
#include "mozilla/dom/Event.h" // for Event
@@ -762,6 +763,13 @@ void nsImageMap::DrawFocus(nsIFrame* aFrame, DrawTarget& aDrawTarget,
762763
void nsImageMap::MaybeUpdateAreas(nsIContent* aContent) {
763764
if (aContent == mMap || mConsiderWholeSubtree) {
764765
UpdateAreas();
766+
767+
// If the mouse cursor hovered an <area> or will hover an <area>, we may
768+
// need to update the cursor and dispatch mouse/pointer boundary events.
769+
// So, let's enqueue a synthesized mouse move.
770+
if (PresShell* const presShell = aContent->OwnerDoc()->GetPresShell()) {
771+
presShell->SynthesizeMouseMove(false);
772+
}
765773
}
766774
}
767775

Lines changed: 376 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,376 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, user-scalable=no">
6+
<title>Event targets of boundary events over an image map</title>
7+
<script src="/resources/testharness.js"></script>
8+
<script src="/resources/testharnessreport.js"></script>
9+
<script src="/resources/testdriver.js"></script>
10+
<script src="/resources/testdriver-actions.js"></script>
11+
<script src="/resources/testdriver-vendor.js"></script>
12+
<script>
13+
"use strict";
14+
15+
addEventListener("load", () => {
16+
const initialDiv = document.getElementById("init");
17+
const container = document.getElementById("container");
18+
const img1 = document.getElementById("img1");
19+
const img2 = document.getElementById("img2");
20+
const area1_1 = document.getElementById("area1-1");
21+
const area1_2 = document.createElement("area");
22+
area1_2.setAttribute("id", "area1-2");
23+
area1_2.setAttribute("shape", "rect");
24+
area1_2.setAttribute("coords", "0,0,100,100");
25+
area1_2.setAttribute("href", "#");
26+
const area2_1 = document.getElementById("area2-1");
27+
const map1 = document.getElementById("map1");
28+
const map2 = document.getElementById("map2");
29+
30+
function stringifyEvents(arrayOfEvents) {
31+
function stringifyEvent(event) {
32+
return `${event.type}@${event.target.localName}${
33+
event.target.id ? `#${event.target.id}` : ""
34+
}`
35+
}
36+
let str = "[";
37+
for (const event of arrayOfEvents) {
38+
if (str != "[") {
39+
str += ", ";
40+
}
41+
str += stringifyEvent(event);
42+
}
43+
return str + "]";
44+
}
45+
46+
let events = [];
47+
function pushEvent(event) {
48+
events.push(event);
49+
}
50+
for (const type of ["pointermove", "pointerover", "pointerenter", "pointerout", "pointerleave"]) {
51+
container.addEventListener(type, pushEvent, {capture: true});
52+
}
53+
54+
promise_test(async () => {
55+
events = [];
56+
await new test_driver.Actions()
57+
.pointerMove(0, 0, {origin: initialDiv})
58+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
59+
.pointerMove(0, 0, {origin: initialDiv})
60+
.send();
61+
assert_equals(
62+
stringifyEvents(events),
63+
stringifyEvents([
64+
{type: "pointerover", target: area1_1},
65+
{type: "pointerenter", target: container},
66+
{type: "pointerenter", target: map1},
67+
{type: "pointerenter", target: area1_1},
68+
{type: "pointermove", target: area1_1},
69+
{type: "pointerout", target: area1_1},
70+
{type: "pointerleave", target: area1_1},
71+
{type: "pointerleave", target: map1},
72+
{type: "pointerleave", target: container},
73+
])
74+
);
75+
}, "pointer boundary events when simple over/out");
76+
77+
promise_test(async () => {
78+
events = [];
79+
await new test_driver.Actions()
80+
.pointerMove(0, 0, {origin: initialDiv})
81+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
82+
.pointerMove(0, 0, {origin: img2}) // actually moved in area1-1
83+
.pointerMove(0, 0, {origin: initialDiv})
84+
.send();
85+
assert_equals(
86+
stringifyEvents(events),
87+
stringifyEvents([
88+
{type: "pointerover", target: area1_1},
89+
{type: "pointerenter", target: container},
90+
{type: "pointerenter", target: map1},
91+
{type: "pointerenter", target: area1_1},
92+
{type: "pointermove", target: area1_1},
93+
// boundary events shouldn't be fired when moving from img1 to img2
94+
{type: "pointermove", target: area1_1},
95+
{type: "pointerout", target: area1_1},
96+
{type: "pointerleave", target: area1_1},
97+
{type: "pointerleave", target: map1},
98+
{type: "pointerleave", target: container},
99+
])
100+
);
101+
}, "pointer boundary events when moved from an <area> to the same <area> shared by another <img>");
102+
103+
promise_test(async t => {
104+
events = [];
105+
function shrinkArea1_1() {
106+
area1_1.setAttribute("coords", "0,0,10,10");
107+
t.add_cleanup(() => {
108+
area1_1.setAttribute("coords", "0,0,100,100");
109+
});
110+
}
111+
area1_1.addEventListener("pointermove", shrinkArea1_1, {once: true});
112+
t.add_cleanup(() => area1_1.removeEventListener("pointermove", shrinkArea1_1));
113+
await new test_driver.Actions()
114+
.pointerMove(0, 0, {origin: initialDiv})
115+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
116+
.addTick(100) // now, over the <img>
117+
.addTick() // for Firefox bug 1994340
118+
.pointerMove(0, 0, {origin: initialDiv})
119+
.send();
120+
assert_equals(
121+
stringifyEvents(events),
122+
stringifyEvents([
123+
{type: "pointerover", target: area1_1},
124+
{type: "pointerenter", target: container},
125+
{type: "pointerenter", target: map1},
126+
{type: "pointerenter", target: area1_1},
127+
{type: "pointermove", target: area1_1},
128+
// Now, the <area> is shrunken and the cursor is not over the <area>.
129+
{type: "pointerout", target: area1_1},
130+
{type: "pointerleave", target: area1_1},
131+
{type: "pointerleave", target: map1},
132+
{type: "pointerover", target: img1},
133+
{type: "pointerenter", target: img1},
134+
// Then, move out from the <img>.
135+
{type: "pointerout", target: img1},
136+
{type: "pointerleave", target: img1},
137+
{type: "pointerleave", target: container},
138+
])
139+
);
140+
}, "pointer boundary events when the <area> is resized");
141+
142+
promise_test(async t => {
143+
events = [];
144+
function shrinkArea1_1() {
145+
area1_1.setAttribute("coords", "0,0,10,10");
146+
img1.setAttribute("width", "200");
147+
img1.getBoundingClientRect();
148+
t.add_cleanup(() => {
149+
img1.setAttribute("width", "100");
150+
area1_1.setAttribute("coords", "0,0,100,100");
151+
img1.getBoundingClientRect();
152+
});
153+
}
154+
area1_1.addEventListener("pointermove", shrinkArea1_1, {once: true});
155+
t.add_cleanup(() => area1_1.removeEventListener("pointermove", shrinkArea1_1));
156+
await new test_driver.Actions()
157+
.pointerMove(0, 0, {origin: initialDiv})
158+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
159+
.addTick() // now over the <img>
160+
.pointerMove(0, 0, {origin: initialDiv})
161+
.send();
162+
assert_equals(
163+
stringifyEvents(events),
164+
stringifyEvents([
165+
{type: "pointerover", target: area1_1},
166+
{type: "pointerenter", target: container},
167+
{type: "pointerenter", target: map1},
168+
{type: "pointerenter", target: area1_1},
169+
{type: "pointermove", target: area1_1},
170+
// Now, the <area> is shrunken and the cursor is not over the <area>.
171+
{type: "pointerout", target: area1_1},
172+
{type: "pointerleave", target: area1_1},
173+
{type: "pointerleave", target: map1},
174+
{type: "pointerover", target: img1},
175+
{type: "pointerenter", target: img1},
176+
// Then, move out from the <img>.
177+
{type: "pointerout", target: img1},
178+
{type: "pointerleave", target: img1},
179+
{type: "pointerleave", target: container},
180+
])
181+
);
182+
}, "pointer boundary events when both <area> and <img> are resized");
183+
184+
promise_test(async t => {
185+
events = [];
186+
function switchMap() {
187+
img1.setAttribute("usemap", "#map2");
188+
t.add_cleanup(() => {
189+
img1.setAttribute("usemap", "#map1");
190+
});
191+
}
192+
area1_1.addEventListener("pointermove", switchMap, {once: true});
193+
t.add_cleanup(() => area1_1.removeEventListener("pointermove", switchMap));
194+
await new test_driver.Actions()
195+
.pointerMove(0, 0, {origin: initialDiv})
196+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
197+
.addTick(100) // now over area2-1
198+
.addTick() // for Firefox bug 1994340
199+
.pointerMove(0, 0, {origin: initialDiv})
200+
.send();
201+
assert_equals(
202+
stringifyEvents(events),
203+
stringifyEvents([
204+
{type: "pointerover", target: area1_1},
205+
{type: "pointerenter", target: container},
206+
{type: "pointerenter", target: map1},
207+
{type: "pointerenter", target: area1_1},
208+
{type: "pointermove", target: area1_1},
209+
// Now, the #map2 is the image map definition and over its area2-1
210+
{type: "pointerout", target: area1_1},
211+
{type: "pointerleave", target: area1_1},
212+
{type: "pointerleave", target: map1},
213+
{type: "pointerover", target: area2_1},
214+
{type: "pointerenter", target: map2},
215+
{type: "pointerenter", target: area2_1},
216+
// Then, move out from the <area>.
217+
{type: "pointerout", target: area2_1},
218+
{type: "pointerleave", target: area2_1},
219+
{type: "pointerleave", target: map2},
220+
{type: "pointerleave", target: container},
221+
])
222+
);
223+
}, "pointer boundary events when usemap is modified");
224+
225+
promise_test(async t => {
226+
events = [];
227+
function switchMap() {
228+
img1.setAttribute("usemap", "#map2");
229+
img1.setAttribute("width", "200");
230+
img1.getBoundingClientRect();
231+
t.add_cleanup(() => {
232+
img1.setAttribute("usemap", "#map1");
233+
img1.setAttribute("width", "100");
234+
img1.getBoundingClientRect();
235+
});
236+
}
237+
area1_1.addEventListener("pointermove", switchMap, {once: true});
238+
t.add_cleanup(() => area1_1.removeEventListener("pointermove", switchMap));
239+
await new test_driver.Actions()
240+
.pointerMove(0, 0, {origin: initialDiv})
241+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
242+
.addTick() // now over area2-1
243+
.pointerMove(0, 0, {origin: initialDiv})
244+
.send();
245+
assert_equals(
246+
stringifyEvents(events),
247+
stringifyEvents([
248+
{type: "pointerover", target: area1_1},
249+
{type: "pointerenter", target: container},
250+
{type: "pointerenter", target: map1},
251+
{type: "pointerenter", target: area1_1},
252+
{type: "pointermove", target: area1_1},
253+
// Now, the #map2 is the image map definition and over its area2-1
254+
{type: "pointerout", target: area1_1},
255+
{type: "pointerleave", target: area1_1},
256+
{type: "pointerleave", target: map1},
257+
{type: "pointerover", target: area2_1},
258+
{type: "pointerenter", target: map2},
259+
{type: "pointerenter", target: area2_1},
260+
// Then, move out from the <area>.
261+
{type: "pointerout", target: area2_1},
262+
{type: "pointerleave", target: area2_1},
263+
{type: "pointerleave", target: map2},
264+
{type: "pointerleave", target: container},
265+
])
266+
);
267+
}, "pointer boundary events when usemap is modified and <img> is resized");
268+
269+
promise_test(async t => {
270+
events = [];
271+
function appendArea1_2() {
272+
map1.insertBefore(area1_2, area1_1);
273+
t.add_cleanup(() => {
274+
area1_2.remove();
275+
});
276+
}
277+
area1_1.addEventListener("pointermove", appendArea1_2, {once: true});
278+
t.add_cleanup(() => area1_1.removeEventListener("pointermove", appendArea1_2));
279+
await new test_driver.Actions()
280+
.pointerMove(0, 0, {origin: initialDiv})
281+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
282+
.addTick(100) // now, over the area1-2
283+
.addTick() // for Firefox bug 1994340
284+
.pointerMove(0, 0, {origin: initialDiv})
285+
.send();
286+
assert_equals(
287+
stringifyEvents(events),
288+
stringifyEvents([
289+
{type: "pointerover", target: area1_1},
290+
{type: "pointerenter", target: container},
291+
{type: "pointerenter", target: map1},
292+
{type: "pointerenter", target: area1_1},
293+
{type: "pointermove", target: area1_1},
294+
// Now, new <area> is inserted and it's the top-most.
295+
{type: "pointerout", target: area1_1},
296+
{type: "pointerleave", target: area1_1},
297+
{type: "pointerover", target: area1_2},
298+
{type: "pointerenter", target: area1_2},
299+
// Then, move out from the <img>.
300+
{type: "pointerout", target: area1_2},
301+
{type: "pointerleave", target: area1_2},
302+
{type: "pointerleave", target: map1},
303+
{type: "pointerleave", target: container},
304+
])
305+
);
306+
}, "pointer boundary events when new <area> is available");
307+
308+
promise_test(async t => {
309+
events = [];
310+
function appendArea1_2() {
311+
map1.insertBefore(area1_2, area1_1);
312+
img1.setAttribute("width", "200");
313+
img1.getBoundingClientRect();
314+
t.add_cleanup(() => {
315+
area1_2.remove();
316+
img1.setAttribute("width", "100");
317+
img1.getBoundingClientRect();
318+
});
319+
}
320+
area1_1.addEventListener("pointermove", appendArea1_2, {once: true});
321+
t.add_cleanup(() => area1_1.removeEventListener("pointermove", appendArea1_2));
322+
await new test_driver.Actions()
323+
.pointerMove(0, 0, {origin: initialDiv})
324+
.pointerMove(0, 0, {origin: img1}) // actually moved over area1-1
325+
.addTick() // now, over the area1-2
326+
.pointerMove(0, 0, {origin: initialDiv})
327+
.send();
328+
assert_equals(
329+
stringifyEvents(events),
330+
stringifyEvents([
331+
{type: "pointerover", target: area1_1},
332+
{type: "pointerenter", target: container},
333+
{type: "pointerenter", target: map1},
334+
{type: "pointerenter", target: area1_1},
335+
{type: "pointermove", target: area1_1},
336+
// Now, new <area> is inserted and it's the top-most.
337+
{type: "pointerout", target: area1_1},
338+
{type: "pointerleave", target: area1_1},
339+
{type: "pointerover", target: area1_2},
340+
{type: "pointerenter", target: area1_2},
341+
// Then, move out from the <img>.
342+
{type: "pointerout", target: area1_2},
343+
{type: "pointerleave", target: area1_2},
344+
{type: "pointerleave", target: map1},
345+
{type: "pointerleave", target: container},
346+
])
347+
);
348+
}, "pointer boundary events when new <area> is available and the <img> is resized");
349+
}, {once: true});
350+
</script>
351+
<style>
352+
img {
353+
margin: 0;
354+
border: none;
355+
}
356+
div {
357+
margin: 0;
358+
width: 200px;
359+
white-space: nowrap;
360+
}
361+
</style>
362+
</head>
363+
<body>
364+
<div id="init">initial position</div>
365+
<div id="container">
366+
<map id="map1">
367+
<area id="area1-1" shape="rect" coords="0,0,100,100" href="#">
368+
</map>
369+
<map id="map2">
370+
<area id="area2-1" shape="rect" coords="0,0,100,100" href="#">
371+
</map>
372+
<img id="img1" usemap="#map1" src="../images/green-16x16.png" width="100" height="100">
373+
<img id="img2" usemap="#map1" src="../images/green-16x16.png" width="100" height="100">
374+
</div>
375+
</body>
376+
</html>

0 commit comments

Comments
 (0)