Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Workaround to force CSS :hover to update after a transition (opening a menu)

There are already several questions addressing this issue. I am including mine for two reasons:

  • It suggests a possible alternative solution
  • The demo code may be useful to others who want to simulate a menu

After a CSS transition, the user must move the mouse before the element that is now under the mouse will notice that it is in a :hover state. I have created a menu-like feature that slides open to show different options. The option under the mouse at the end of the opening transition is not the same as the one under the mouse at the start of the transition. I have thus had to find a workaround.

You can find a jsFiddle here and the demo source below. Look for WORKAROUND (in three places) to see what I have done.

To see the issue, move the mouse over the menu and then leave it in place, without moving it. The list item that the browser thinks is :hover will appear in blue. My workaround overrules the li:hover rule with an li.ignoreHover class. To make the workaround invisible, I can simply use the standard background colour. Instead, I am using blue to make the issue visible.

My question: I have noticed that pressing one of the modifier keys (Caps, Caps lock, Ctrl, Option/Alt, on Mac, ...) will also force the :hover state to update. Is there a way to send such an event to the #menu element?

(My attempts to do so have not been successful, so I prefer to give you my working workaround than one that may not be valid).

<!DOCTYPE html>
<html>
<head>
 <style>
#menu {
  position: relative;
  background: #ccc;
  display: inline-block;
}
#wrapper {
  margin: 5px;
}
#logo {
  width: 150px;
  height: 50px;
  border: 1px solid #000;
  margin: 0px auto;
  z-index: 10;
}
nav {
  width: 100%;
  overflow: hidden;
  text-align:center;
  height: 2em;
}
ul {
  position: relative;
  display:inline-block;
  margin: 0 auto;
  padding: 0;
  list-style-type: none;
  text-align:left;
}
li {
  display: block;
  margin: 0;
  padding:0.25em 0;
  line-height: 1.5em;
}
ul.animated, nav {
  transition: all 500ms linear 1s;
}
#menu.hover ul, #menu.hover nav {
  transition-delay: 0s;
}
li:hover,
li.hover {
  background-color: #999;
}
li.ignoreHover {
  background-color: #ccf; /* a touch of blue, so you can see it */
}
.selected {
  color: #fff;
}
 </style>
  <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> 
</head>
<body>
<div id="menu">
  <div id="wrapper">
    <div id="logo"></div>
  </div>
  <nav>
    <ul>
      <li>Note one</li>
      <li>Note two</li>
      <li>Note three</li>
      <li>Not four much longer</li>
      <li>Note five</li>
      <li>Note six</li>
    </ul>
  </nav>
</div>

<script>
var test = {}
;(function createMenu() {
    var item = 3;
    var minPadding = 5;
    var hover = "hover" // class

    var $li = $("li");
    var $ul = $("ul");
    var $menu = $("#menu");
    var $nav = $("nav");

    var itemHeight = parseInt($li.outerHeight(), 10);
    var itemCount = $ul.children().length;
    var menuWidth = $menu.outerWidth(true);
    var padding = (menuWidth - $ul.width()) / 2;

    var transitionDone = false;
    var mouseOver = false;
    var top;

    // Pad the list items to fill the width of the menu
    if (padding < minPadding) {
        // Widen the menu to allow for the minimum padding
        menuWidth += (minPadding - padding) * 2;
        $menu.width(menuWidth);
        padding = minPadding;
    }

    $li.css({
        paddingLeft: padding,
        paddingRight: padding
    });

    // Scroll to the current selected item
    selectItem(true);

    function selectItem(scroll) {
        $ul.children().removeClass("selected");
        $ul.children().eq(item).addClass("selected");

        if (scroll) {
            top = -(itemHeight * item);
            $ul.css({
                top: top
            });
        }
    }

    // Wait until the initial settings are applied 
    // before animating the transitions
    setTimeout(function () {
        $ul.addClass("animated");
    }, 1);

    // Handle interaction with the menu
    $menu.on("mouseover", openMenu);
    $menu.on("mouseleave", closeMenu);
    $menu.on("transitionend", menuIsOpen);
    $ul.on("click", treatClickOnItem);

    // <WORKAROUND...
    var x
    var y
    // ... WORKAROUND>


  function openMenu(event) {

        if (mouseOver) {
            // This method may be called multiple times as the menu is
            // transitioning to its open state
            return
        }

    // <WORKAROUND...
    $menu.on("mousemove", function updateXY(event) {
            x = event.pageX
            y = event.pageY
    })
    // ... WORKAROUND>

        $menu.addClass(hover);
        transitionDone = false;
        mouseOver = true;

        $nav.css({
            height: (itemHeight * itemCount)
        });
        $ul.css({
            top: 0
        });
    }

    function menuIsOpen() {
        transitionDone = true;

      // <WORKAROUND...   
    var $hover = $("li:hover").addClass("ignoreHover")
    var $item = $(document.elementFromPoint(x, y))
    if (mouseOver) {
      $item.addClass(hover)
    }
    $menu.on("mousemove", function () {
      $item.removeClass(hover)
      $hover.removeClass("ignoreHover")
      $menu.off("mousemove")
    })
        //... WORKAROUND>

        if (!mouseOver) {
            closeMenu()
        }
    }

    function closeMenu() {
        mouseOver = false;
        if (transitionDone) {
            $menu.removeClass(hover)

            $nav.css({
                height: itemHeight
            });
            $ul.css({
                top: top
            });
        }
  }

    function treatClickOnItem(event) {
        item = $(event.target).index();
        top = -(itemHeight * item);
        selectItem();
        // DO MORE STUFF WITH THE SELECTION
    }
})()
</script>
</body>
</html>
like image 532
James Newton Avatar asked May 24 '15 18:05

James Newton


1 Answers

jsBin demo

Seems like it's almost impossible to get the :hover state of an element while it's animating.

Remove that :hover from CSS and
create instead a class .hover with the desired styles. use jQuery to toggle .hover:

$links.hover(function(){
  $(this).toggleClass("hover");
});

Now back to your issue:

In order to get the right element highlighted once the menu opens
we need to always know the mouse Y position:

var mouseY = 0; // Needed to know the mouse position when menu is opening
$(document).on("mousemove", function( e ){
    mouseY = e.clientY; // Update the Y value
});

now, on collapsed menu hover, animate your menu using jQuery,
inside the animate step callback get on every frame each link position, .filter() them by targeting the one that matches the mouse position.
And finally apply the .hover to only that one:

function openMenu() {
  $navUl.stop().animate({top: 0});
  $nav.stop().animate({height: linkH*nLinks}, {
    duration: 600,
    step: function( menuHeight ){
        // keeps removing and adding class during the animation time.
        // (it's an overkill but no other solution to that issue so far)
        $links.removeClass("hover").filter(function(i, e){
          var t = e.getBoundingClientRect().top;
          return mouseY > t  &&  mouseY < t+linkH;
        }).addClass("hover"); // only to the link returned by `.filter()` condition
    }
  });
}

! important note: the above filtering will be as expensive as many items you have, cause at every animate frame it tries to get the positions. If you detect slowliness - improve the above.

To recap
at every frame check if the mouse clientX/Y coordinates are inside the element's element.getBoundingClientRect() coordinates/values

like image 60
Roko C. Buljan Avatar answered Sep 21 '22 08:09

Roko C. Buljan