Interactions - Trigger an animation when dropdown menu loses focus

I’m making a navigation menu with a dropdown using PG Interactions and am having trouble making the dropdown hide when it loses keyboard focus. Hover, touch, and Escape work as expected, and I can trigger and navigate the dropdown using my keyboard, but I can’t figure out how to make it hide when I tab away from the items in the dropdown menu.

Here is a loom video showing what happens.

PG Interactions Menu Keyboard Focus Blur Issue - Watch Video

And here is a repo with a test project:

Hey @adamslowe,

that one is a great possibility for me to learn something about focus. I always thought it’s about tabindex and accessibility. Never heard of focus() and blur() before.

I’ve developed a quick snippet which should do exactly what you want to achieve. It’s based on the discussion here javascript - Is there a cross-browser solution for monitoring when the document.activeElement changes? - Stack Overflow.

What is basically does is adding a eventlistener to the window which listens to every focus() event. Whenever a focus is called it checks if the current focused element ( document.activelement ) is the same as the last item in your dropdown.

If the currently focused element is the last element in the dropdown it adds another eventlistener which listens to the blur() events.

So the next time the blur event get’s called it probably means that the dropdown should get closed. So far so good.

EDIT: While I was writing the lines above I realized that there could be a case where you move backwards, in that case the solution above would also close the dropdown.

What I’ve found after another research is the eventlistener (‘focusin’). Don’t know if focus also holds that information but focusin seemed perfect here! :wink: So with focusin you have access to the target of the next focus and also to the current focused element. With that I was able to write a small javascript-snippet which worked in my tests perfectly fine!

The javascript-code ready to copy and paste is here (I’ve also added the eventlistener DOMContentLoaded, which should prevent render-blocking if you place it in the head of your file, but you should place that file at the bottom of your body! :wink: ) And a few lines underneath the javascript code there would be a full HTML-Example to copy & paste where you can test that. The last input of the long row should simulate your last dropdown-item!

JS-Snippet:

      document.addEventListener("DOMContentLoaded", () => {
        // VARIABLES:
        var dropdownWrapper = document.querySelector("#dropdown-wrapper");
        var dropdownItems = document.querySelectorAll(".dropdown-item");
        var lastItem = dropdownItems[dropdownItems.length - 1];

        addEventListener("focusin", (event) => {
          var hideDropdown = false;
          // Means we are now jumping from the last dropdown-item in some direction:
          if (event.relatedTarget == lastItem) {
            var hideDropdown = true;

            // Check if we move backwards, because if we jump to the element before the last dropdown element we probably don't want to hide our dropdown! ;-)
            dropdownItems.forEach((item) => {
              // Set the variable hideDropdown to false if we move backwards (means one of the loop items is the target of the next focus);
              if (item == event.target) {
                hideDropdown = false;
              }
            });

            // After we looped through each element and are sure that we are moving forward we can hide the dropdown element:
            if (hideDropdown) {
              dropdownWrapper.style.display = "none";
            }
          }
        });
      });

Full HTML (For Testing):

<html>
  <body>
    <input type="text" id="Nav-Item1" name="Text2" value="" />

    <div id="dropdown-wrapper">
      <input type="text" id="1" class="dropdown-item" name="Text1" value="" />
      <input type="text" id="2" class="dropdown-item" name="Text1" value="" />
      <input type="text" id="3" class="dropdown-item" name="Text1" value="" />
      <input type="text" id="lastitem" class="dropdown-item" name="Text1" value="" />
    </div>

    <input type="text" id="Nav-Item2" name="Text2" value="" />
    <script>
      document.addEventListener("DOMContentLoaded", () => {
        // VARIABLES:
        var dropdownWrapper = document.querySelector("#dropdown-wrapper");
        var dropdownItems = document.querySelectorAll(".dropdown-item");
        var lastItem = dropdownItems[dropdownItems.length - 1];

        addEventListener("focusin", (event) => {
          var hideDropdown = false;
          // Means we are now jumping from the last dropdown-item in some direction:
          if (event.relatedTarget == lastItem) {
            var hideDropdown = true;

            // Check if we move backwards, because if we jump to the element before the last dropdown element we probably don't want to hide our dropdown! ;-)
            dropdownItems.forEach((item) => {
              // Set the variable hideDropdown to false if we move backwards (means one of the loop items is the target of the next focus);
              if (item == event.target) {
                hideDropdown = false;
              }
            });

            // After we looped through each element and are sure that we are moving forward we can hide the dropdown element:
            if (hideDropdown) {
              dropdownWrapper.style.display = "none";
            }
          }
        });
      });
    </script>
  </body>
</html>

Short explanation in very bad quality :smiley:

@Wolfgang.Hartl You are a rockstar, man! Thank you for your help. I was headed down that path myself before I decided to raise the flag and ask for help, so I’m glad to see I wasn’t on (another) wild goose chase.

I’m going to need a minute to digest this and figure out how to make this work with PG Interactions, but I’m sure it can be done.

I’m happy to see that you are active and interested in Pinegrow. Your contributions in the Bricks community have always been great and I always love seeing strong and vocal advocates for Pinegrow. I’m only using a fraction of Pinegrow’s capabilities and it’s already been a game changer for me this year.

The more we work together, the better the community reach and growth, and the more effort Matjaz and the Pinegrow team can put toward the things that matter to us all!

1 Like

Thanks for your kind words! As always it’s a high-feeling for me if I can help others and what I enjoy about a community is that it’s always taking & giving. Just found two tutorials of yours today which I enjoyed to watch. Specially this one here Display ANY WordPress Custom Field using Pinegrow - YouTube was very interesting to know. Since i could basically build the same concept with that information as I already use with Bricks or other page-builders! But the gutenberg-thing really catched my attention. It’s fun to play around with it and I’m already doing a project with it! :slight_smile:

Probably you can achieve that with interactions too, but that was a bit too overwhelming for me in the first place so i decided to go with the fake-“LTD” option first and will upgrade after my first year when I feel more comfortable with the UI itself. Thought I could still use motion.page meanwhile! :wink:

1 Like

@Wolfgang.Hartl In case you missed this one, here is a great link from W3C about accessible menus.

They had a solution similar to yours, pasted below.

var menuItems = document.querySelectorAll('li.has-submenu');
Array.prototype.forEach.call(menuItems, function(el, i){
	el.querySelector('a').addEventListener("click",  function(event){
		if (this.parentNode.className == "has-submenu") {
			this.parentNode.className = "has-submenu open";
			this.setAttribute('aria-expanded', "true");
		} else {
			this.parentNode.className = "has-submenu";
			this.setAttribute('aria-expanded', "false");
		}
		event.preventDefault();
		return false;
	});
});

I’ve had to piecemeal so much of this stuff together since the WCAG is a mess of rules and contradictions. I wish there was a definitive guide on what to do in certain situations. Don’t even get me started on the mess of WAI-ARIA roles!

Oh yeah, i totally missed that. But it seems that this snippet only listens to clicks not to focus-changes! So you could probably combine mine with that one and create that aria-stuff then too! :wink:

Yeah accessibility is one of the topics I could spit whenever I hear it. I always avoid doing there as much as possible, but sometimes it’s not avoidable, unfortunately! :smiley:

@Wolfgang.Hartl If I’m not mistaken, you can get an add-on subscription for Interactions (and maybe WooCommerce?) that works with the One-Time Payment option. Open the Purchase & Activate screen from the support menu and you’ll probably find it. At least that’s what it says on the Pinegrow Buy link.

Yeah thanks. That was my plan! But during the testing-period I was a bit overchallenged with the UI, so that’s why I decided to learn the basics first and stick with the essential-stuff and later on I can learn more about it. :slight_smile:

1 Like