Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Hovering zero-height element: browser bug?

While working on a submenu that (un)folds as users hover a parent menu item, I ran into the following issue. Consider the following markup (below or on JSFiddle).

ul {
  max-height: 0;
  overflow: hidden;
  margin: 10px;
  padding: 0;
  border: 10px solid #bada55;
  list-style-type: none;
  transition: border-color .4s;
}
ul:hover {
  max-height: 100px;
  border-color: #de2d26;
}

ul > li {
  padding: 10px;
  pointer-events: auto;
}

.ignorant {
  pointer-events: none;
}

.offset {
  position: relative; /* This is the culprit! */
}
<ul class="ignorant">
  <li>I am ignorant of your pointer.
      If you see me, something went wrong!</li>
</ul>
<ul>
  <li>I am not. Hover me!</li>
</ul>
<ul class="ignorant offset">
  <li>I should be ignorant of your pointer.
      If you see me, something went wrong!</li>
</ul>

Setup. We have three lists here of which the contents are hidden, because the max-height of the lists is set to 0. The borders are still shown, which is expected behavior: the height directive of an element by default only applies to the content of the element.

We show the contents on hover of the list, by setting the max-height to some big enough value.

Now, I want to make the list visible only when the contents are hovered, not the borders. This, I believe, should be achievable using pointer-events as shown above. Ignore events on the borders and re-enable them on the items in the lists.

Problem. So far, so good, this works fine in Firefox and mostly in Chrome. Namely, when I position the container relative, it is possible to trigger a hover of the content in Chrome.

Firefox still works as I would expect it to, not triggering a hover. When having no borders, triggering a hover is no longer possible in Chrome either.

Question. Is this a bug in Chrome? Or is it not clearly specified how this should be handled and therefore undefined behavior? I would be interested in any of the following:
1. an explanation of what I am doing wrong, and why;
2. an explanation of why it is okay for different browsers to behave differently in this case;
3. a brief explanation of why this is a bug. In this case, I'd be happy to report one.


This is tested, at the moment, with
&bullet; Firefox 52.0.1 (64-bit Linux N);
&bullet; Firefox 52.0.2 (64-bit Windows N);
&bullet; Internet Explorer 11.0.9600.17031 (64-bit Windows N);
&bullet; Chrome 57.0.2987.133 (64-bit Linux S, 64-bit Windows S);
&bullet; Chromium 57.0.2987.98 (64-bit Linux S).
S means shown, N means not shown on hover, when using relative positioning.

Thanks to BoltClock for making this question better through their constructive comments.

like image 347
Just a student Avatar asked Apr 03 '17 11:04

Just a student


2 Answers

Hover event bubbles the same way on all browsers which is proven by this example.

ul {
  max-height: 0;
  /* overflow: hidden; */
  margin: 10px;
  padding: 0;
  border: 10px solid #bada55;
  list-style-type: none;
  transition: border-color .4s;
}

ul:hover {
  max-height: 100px;
  border-color: #de2d26;
}

ul > li {
  padding: 10px;
  pointer-events: auto;
  position: relative;
  top: 100px;
  background: yellow;
}

.ignorant {
  pointer-events: none;
}

.offset {
  position: relative;
}
<ul class="ignorant offset">
  <li>Hover me! Testing :hover across browsers...</li>
</ul>

What I concluded:

  • it doesn't matter what elements we use, what matters is their position and display property;
  • it doesn't happen when child has position: relative or display: inline.

Hover is fired only when mouse is over the area where child's box model covers same pixels as its parent's box model. This area can be seen on the screen just above the blue rectangle. Don't even say it's aqua or something, I'm a simple programmer, I have no idea about colors.

common box area fires hover event

Chrome hides child element because of its parent's overflow: hidden property which is expected behavior. What's unexpected is child's event box (if I can call it that) is still "visible" and hovers over it's parent's border just like it would with overflow: visible.

The strange thing is it doesn't happen, as I stated before, when child has position: relative or display other than block.

According to W3C:

Once a box has been laid out according to the normal flow or floated, it may be shifted relative to this position. This is called relative positioning. Offsetting a box (B1) in this way has no effect on the box (B2) that follows: B2 is given a position as if B1 were not offset and B2 is not re-positioned after B1's offset is applied.

Which, to my understanding, implies that position: relative should have no effect on the boxes. The fact that it has proves that it's a Chrome bug.

As to the pointer events. MDN states:

Note that preventing an element from being the target of mouse events by using pointer-events does not necessarily mean that mouse event listeners on that element cannot or will not be triggered. If one of the element's children has pointer-events explicitly set to allow that child to be the target of mouse events, then any events targeting that child will pass through the parent as the event travels along the parent chain, and trigger event listeners on the parent as appropriate. Of course any mouse activity at a point on the screen that is covered by the parent but not by the child will not be caught by either the child or the parent (it will go "through" the parent and target whatever is underneath).

Which means Chrome is not showing the child's event box on top of its parent but actually below it. It's just that the event goes through the parent element and hits its child. This shouldn't happen with overflow: hidden though.

like image 152
Linek Avatar answered Oct 24 '22 20:10

Linek


I don't have an official explanation for this, but I know it has to do with block formatting contexts (MDN, W3C). I also am quite confident @BoltClock, who has a thorough understanding of BFCs (I sometimes suspect him for writing the spec in the first place), will be able to provide an exact technical explanation, should he ever wish to do so.

In short, knowing the source is knowing the fix (tested and works): when you need to set position:relative on the parent, also set position:relative on the children:

/* ... */
.offset {
  position: relative; /* This is the culprit! */
}
.offset > * {
  position: relative; /* And this is the fix! */
}

ul {
  max-height: 0;
  overflow: hidden;
  margin: 10px;
  padding: 0;
  border: 10px solid #bada55;
  list-style-type: none;
  transition: border-color .4s;
}
ul:hover {
  max-height: 100px;
  border-color: #de2d26;
}

ul > li {
  padding: 10px;
  pointer-events: auto;
}

.ignorant {
  pointer-events: none;
}

.offset {
  position: relative; /* This is the culprit! */
}
.offset > * {
  position: relative; /* And this is the fix! */
}
<ul class="ignorant">
  <li>I am ignorant of your pointer.
      If you see me, something went wrong!</li>
</ul>
<ul>
  <li>I am not. Hover me!</li>
</ul>
<ul class="ignorant offset">
  <li>I should be ignorant of your pointer.
      If you see me, something went wrong!</li>
</ul>

That's what you wanted, right? Cross browser.


Note: it doesn't have to be relative. It has to be anything but static.

like image 30
tao Avatar answered Oct 24 '22 20:10

tao