Blog-Archiv

Sonntag, 8. Oktober 2017

JS Dropdown Gotcha

The difference between a popup and a dropdown may be that the popup doesn't have a visible trigger, it pops up wherever the mouse was right-clicked, while a dropdown appears on left mouse click on a specific visible trigger button.
Popup also may be a synonym for a dialog window.
Popup menus are also called context menus, because they are frequently used in bulk views like lists, tables, trees and tree-tables, where the triggered menu action refers to the selected items only (→context).

This Blog is about a simple dropdown list implementation for a web page, and why it doesn't always work as expected. By the way, you can always use the browser-native dropdown element!

A Simple Dropdown

Demo

Left-click onto the buttons below to see dropdowns.

Tests:
  1. Click a "Dropdown".
    Expected: a dropdown list gets visible.

  2. Open a "Dropdown". Click anywhere but not in a "Dropdown" or a scrollbar.
    Expected: the open dropdown closes.

  3. Open a "Dropdown". Open another "Dropdown".
    Expected: first dropdown closes when second opens.

Source Code

HTML

    <div class="dropdown-button">
      Dropdown 1
      <div class="dropdown-content">
        <a href="#home">Home 1</a><br>
        <a href="#about">About 1</a><br>
        <a href="#contact">Contact 1</a>
      </div>
    </div>

The dropdown trigger is marked with CSS class dropdown-button. The dropdown-list is a nested element of the trigger, tagged by CSS class dropdown-content.

CSS

      .dropdown-content {
        display: none; /* initially invisible */
        position: absolute; /* not in layout flow */

        /* following is not important */
        border: 1px solid gray;
        background-color: white;
        padding: 0.3em;
      }
      .dropdown-button {
        /* not important */
        display: inline-block;
        border: 1px solid red;
        padding: 0.3em;
      }
      .dropdown-button::after {
        content: "\25BC"; /* the right-side arrow */
      }

The CSS is not really important here, except position: absolute, which takes the element out of the normal layout flow, and display: none, which makes it invisible and being ignored by the layout.

JavaScript

    (function() {
    
      var closeAllDropdowns = function(event, exceptElement) {
          var dropdownContents = document.getElementsByClassName("dropdown-content");
          for (var i = 0; i < dropdownContents.length; i++) {
            var dropdownContent = dropdownContents[i];
            if (exceptElement !== dropdownContent)
              dropdownContent.style.display = "none";
          }
      };
      
      var clickDropdownButton = function(event) {
          event.stopPropagation();
          
          var contentParent = (event.target || event.srcElement);
          var dropdownContent = contentParent.querySelector(".dropdown-content");
          var isHidden = (window.getComputedStyle(dropdownContent).display === "none");
          
          closeAllDropdowns(event, dropdownContent);
          dropdownContent.style.display = isHidden ? "block" : "none";
      };
        
      /* initialization: find all dropdowns on page and prepare them */
            
      var dropdownButtons = document.getElementsByClassName("dropdown-button");
      for (var i = 0; i < dropdownButtons.length; i++)
        dropdownButtons[i].addEventListener("click", clickDropdownButton); 
      
      window.addEventListener("click", closeAllDropdowns);
      
    })();

The closeAllDropdowns() function closes all dropdowns on page. It can be called with two optional parameters, the second being a dropdown that should be ignored.

The clickDropdownButton() function is called when the dropdown-button gets clicked. Because a window-listener for clicks would close all dropdowns, it stops the event bubbling. Then it finds the dropdown from the event target and displays or closes it, depending on current state, after having closed all other possibly open dropdowns.

The initialization loops all dropdown-buttons and installs click-listeners to them that toggle the dropdown-content. Another click-listener gets installed onto the window object, closing any possibly open dropdown. (Mind that other elements on the page could stop the propagation of clicks onto them, and thus prevent the dropdowns closing!)

The Gotcha

Now somebody, for some reason, changes the CSS of the dropdown-button to following:

      .dropdown-button {
        position: relative;
        overflow: hidden;

        .... /* rest unchanged */
      }

Here is the result. I made the button a little bigger to show the obscured dropdown inside. Click to see it.

What happened?

The dropdown-content was positioned absolute, so it gets positioned relatively to the nearest parent with a non-static position. For the original implementation this was the body element. Because no top and left coordinate had been put onto the dropdown-content, this worked, it kept its original coordinates and was displayed at the correct location.

But now the nearest parent with a non-static position is the dropdown-button, due to position relative. This alone does no harm, because the coordinates of the dropdown-content still are the original ones. But the the dropdown-button was also restricted to hide its overflow. No child element can go outside of such a parent's bounds.

To fix this, you must remove either position: relative or overflow: hidden. If this is not possible, rewrite the dropdown to the solution that I presented in my last Blog.

Resume

One thing always comes to my mind when I meet such problems:

  • CSS is nice for colors, borders, fonts and icons, but not for doing layout!



Keine Kommentare: