1

I have a simple SVG that contains a triangle and toggles its fill color when it's clicked:

const toggleFill = (element) => {
  if (element.style.fill === 'silver') {
    element.style.fill = 'grey';
  } else {
    element.style.fill = 'silver';
  }
};
const svg = document.querySelector('svg');
svg.addEventListener('click', () => toggleFill(svg));
svg {
  height: 70px;
  width: 80px;
}
<svg viewBox="0 0 90 78" style="fill: silver">
  <polygon points="0 78, 45 0, 90 78" stroke="black"/>
</svg>

The problem is that clicking anywhere within the bounding box of the SVG (outside of the triangle) also triggers the click event. I'd like to restrict it to only toggle the fill color when the triangle itself is clicked, not its transparent background.

According to this related question my desired behavior should be the default behavior, since pointer-events is supposed to default to the visiblePainted behavior. Nonetheless, that's not what I see in either Firefox or Chrome, and even setting it explicitly has no effect.

Altay_H
  • 489
  • 6
  • 14
  • Have you considered binding click events on the elements you're painting, e.g. `document.querySelector('svg polygon').addEventListener...`? – Terry Sep 29 '22 at 21:41
  • @Terry In my actual code I'm changing the contents of the SVG programmatically, so it's much more convenient to have the click handler bound to the SVG element itself. – Altay_H Sep 29 '22 at 21:53
  • If that's the case can't you check `e.target` to see what is being clicked on? If `e.target === e.currentTarget` (i.e. SVG is being clicked) then you do nothing – Terry Sep 29 '22 at 21:57
  • The trouble with that approach is that I'm tessellating these triangles and their bounding boxes overlap, so the `e.target` is actually a different SVG element than the one that was clicked. – Altay_H Sep 29 '22 at 22:11
  • Your testcase needs to refect your actual problem in that case, otherwise you'll get all sorts of answers that you can't use. – Robert Longson Sep 30 '22 at 12:22

1 Answers1

3

The problem here is that the <svg> element is part of a HTML page. There, the behavior of hit-testing is undefined:

This specification does not define the behavior of pointer events on the outermost svg element for SVG images which are embedded by reference or inclusion within another document, e.g., whether the outermost svg element embedded in an HTML document intercepts mouse click events; future specifications may define this behavior, but for the purpose of this specification, the behavior is implementation-specific.

If you look up pointer-events in a HTML context, you will find more or less nothing:

While this property modifies the normal behavior of hit-testing, this normal hit-testing is currently not specified. There is broad interoperability about the seemingly obvious parts of this problem, but there are countless nuances and corner cases that would greatly benefit from a detailed specification. The CSS-WG would hugely appreciate help with writing such a specification.

So for now it seems browsers treat the outermost <svg> element like any any other box and click events are captured within the whole border-box.

But this is only true for the outermost <svg> element. Nest another <svg> inside, (or better a <g> element) and attach the event listener to that, and the expected default behavior of paintedVisible is restored:

const toggleFill = (element) => {
  if (element.style.fill === 'silver') {
    element.style.fill = 'grey';
  } else {
    element.style.fill = 'silver';
  }
};
const g = document.querySelector('svg > g');
g.addEventListener('click', () => toggleFill(g));
svg {
  height: 70px;
  width: 80px;
}
<svg viewBox="0 0 90 78">
  <g style="fill: silver">
    <polygon points="0 78, 45 0, 90 78" stroke="black"/>
  </g>
</svg>
ccprog
  • 20,308
  • 4
  • 27
  • 44