Child element's click event prevented by parent's event.stopPropagation

135 views Asked by At

On my (live site), I have a select element created using the 'nice-select' library. This select element is nested inside a dropdown menu, which has its own JavaScript file

    function singleMenu(targetId, menuId, show = false) {
    const targetElement = document.getElementById(targetId);
    const menuElement = document.getElementById(menuId);

    // Initial state
    if (show) {
        // show dropdown
        menuElement.style.display = "block";
        targetElement.classList.add("active");
    } else {
        // hide dropdown
        menuElement.style.display = "none";
        targetElement.classList.remove("active");
    }

    // Toggle menu visibility when target element is clicked
    targetElement.addEventListener("click", () => {
        show = !show;

        if (show) {
            // show dropdown
            menuElement.style.display = "block";
            targetElement.classList.add("active");
        } else {
            // hide dropdown
            menuElement.style.display = "none";
            targetElement.classList.remove("active");
        }
    });

    // Close menu if clicked outside of container
    document.addEventListener("click", (event) => {
        if (!targetElement.contains(event.target)) {
            show = false;
            menuElement.style.display = "none";
            targetElement.classList.remove("active");
        }
    });

    // Prevent menu from closing when clicked inside the menu element
    menuElement.addEventListener("click", function (event) {
        event.stopPropagation();
    });

    // Calculate half of the targetElement width
    const targetHalfWidth = targetElement.offsetWidth / 2;

    // Set a CSS variable with the half width value
    targetElement.style.setProperty(
        "--target-half-width",
        targetHalfWidth + "px"
    );
}

Currently, I'm facing an issue where the select element does not open when clicked, but it does work properly when using the enter and up/down buttons.

I suspect that the problem might be related to the event.stopPropagation() function, as when I remove that line, the select element's 'open' class gets added (as seen in the inspect tool). However, removing the event.stopPropagation() line also results in the dropdown menu closing immediately.

My goal is to find a solution that allows me to open the select elements without closing the dropdown menu. I would greatly appreciate any insights or suggestions on how to achieve this.

Here is the relevant HTML code (html file code) for reference. Thank you for your help!

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link
            rel="stylesheet"
            id="hello-elementor-child-style-css"
            href="https://fs.codelinden.com/wp-content/themes/hello-child/style.css?ver=3"
            media="all"
        />
        <link rel="stylesheet" href="header.css" />
        <script src="header.js" defer></script>

        <!-- single-menu dropdown style -->
        <link rel="stylesheet" href="single-menu-dropdown.css" />

        <!-- custom form element style (nice-select) -->
        <link rel="stylesheet" href="form.css" />

        <!-- nice-select style-->
        <link
            rel="stylesheet"
            href="https://cdnjs.cloudflare.com/ajax/libs/jquery-nice-select/1.1.0/css/nice-select.min.css"
            integrity="sha512-CruCP+TD3yXzlvvijET8wV5WxxEh5H8P4cmz0RFbKK6FlZ2sYl3AEsKlLPHbniXKSrDdFewhbmBK5skbdsASbQ=="
            crossorigin="anonymous"
            referrerpolicy="no-referrer"
        />

        <title>Header</title>
    </head>
    <body>
        <header class="header HruDj">
            <div class="nav-dropdown target-id DeYlt" id="target_id1">
                <p>toggle dropdown</p>

                <!-- single-menu dropdown container -->
                <div id="menu_id1" class="menu-id">
                    <div class="form-field full-width">
                        <input
                            type="hidden"
                            name="form_my_contact_form"
                            value="1"
                        />
                        <label for="product_type">Product Type</label>
                        <select
                            name="product_type"
                            id="product_type"
                            required=""
                        >
                            <option value="book">Book</option>
                            <option value="movie">Movie</option>
                            <option value="music">Music</option>
                            <option value="" disabled="">
                                Select a product type
                            </option>
                        </select>
                    </div>
                </div>
            </div>
            <div class="nav-dropdown target-id DeYlt" id="target_id2">
                <p>toggle dropdown</p>

                <!-- single-menu dropdown container -->
                <div id="menu_id2" class="menu-id">
                    <div class="form-field full-width">
                        <input
                            type="hidden"
                            name="form_my_contact_form"
                            value="1"
                        />
                        <label for="product_type">Product Type</label>
                        <select
                            name="product_type"
                            id="product_type"
                            required=""
                        >
                            <option value="book">Book</option>
                            <option value="movie">Movie</option>
                            <option value="music">Music</option>
                            <option value="" disabled="">
                                Select a product type
                            </option>
                        </select>
                    </div>
                </div>
            </div>
        </div>
        </header>

        <!-- jquery cdn -->
        <script src="popup/jquery/jquery.min.js"></script>

        <!-- single-menu dropdown script -->
        <script src="single-menu-dropdown.js"></script>

        <!-- nice-select script -->
        <script
            src="https://cdnjs.cloudflare.com/ajax/libs/jquery-nice-select/1.1.0/js/jquery.nice-select.min.js"
            integrity="sha512-NqYds8su6jivy1/WLoW8x1tZMRD7/1ZfhWG/jcRQLOzV1k1rIODCpMgoBnar5QXshKJGV7vi0LXLNXPoFsM5Zg=="
            crossorigin="anonymous"
            referrerpolicy="no-referrer"
        ></script>

        <!-- initialize dropdown and select -->
        <script>
            // Call singleMenu function for each menu
            singleMenu("target_id1", "menu_id1", false);
            singleMenu("target_id2", "menu_id2", false);

            $(document).ready(function () {
                // Apply the niceSelect plugin to all select elements
                $("select").niceSelect();
            });
        </script>
    </body>
</html>
3

There are 3 answers

0
deepanshu223 On BEST ANSWER

So I was able to arrive at the desired result but with some small changes. There didn't seem to be any approach with the event.stopPropagation() that would work consistently.

What you can do is that instead of setting the whole menu parent as the click handler set it to work on the more direct target instead.

This means that instead of using

 <div class="nav-dropdown target-id DeYlt" id="target_id1">
     <p>toggle dropdown</p>

You would want to add the target id to the p tag inside.

 <div class="nav-dropdown target-id DeYlt">
     <p id="target_id1">toggle dropdown</p>

Same for the menu_id2 and target_id2.

This will require a small change in the javascript. Change the click handler that closes the menu when clicking outside to handle targetElement.parentElement instead to account for the target being within the menu now.

 // Close menu if clicked outside of container
    document.addEventListener("click", (event) => {
        if (!targetElement.parentElement.contains(event.target)) {
            show = false;
            menuElement.style.display = "none";
            targetElement.classList.remove("active");
        }
});

Get rid of the menu stop propagation item as it's no longer needed.

menuElement.addEventListener("click", function (event) {
    event.stopPropagation();

All in all, here's what an updated code from your question looks like which should work as expected.

function singleMenu(targetId, menuId, show = false) {
    const targetElement = document.getElementById(targetId);
    const menuElement = document.getElementById(menuId);

    // Initial state
    if (show) {
        // show dropdown
        menuElement.style.display = "block";
        targetElement.classList.add("active");
    } else {
        // hide dropdown
        menuElement.style.display = "none";
        targetElement.classList.remove("active");
    }

    // Toggle menu visibility when target element is clicked
    targetElement.addEventListener("click", () => {
        show = !show;

        if (show) {
            // show dropdown
            menuElement.style.display = "block";
            targetElement.classList.add("active");
        } else {
            // hide dropdown
            menuElement.style.display = "none";
            targetElement.classList.remove("active");
        }
    });

    // Close menu if clicked outside of container
    document.addEventListener("click", (event) => {
        if (!targetElement.parentElement.contains(event.target)) {
            show = false;
            menuElement.style.display = "none";
            targetElement.classList.remove("active");
        }
    });

    // Prevent menu from closing when clicked inside the menu element
   /* menuElement.addEventListener("click", function (event) {
        event.stopPropagation();
    });*/

    // Calculate half of the targetElement width
    const targetHalfWidth = targetElement.offsetWidth / 2;

    // Set a CSS variable with the half width value
    targetElement.style.setProperty(
        "--target-half-width",
        targetHalfWidth + "px"
    );
}


 singleMenu("target_id1", "menu_id1", false);
 singleMenu("target_id2", "menu_id2", false);
 $(document).ready(function () {
   // Apply the niceSelect plugin to all select elements
   $("select").niceSelect();
 });
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link
            rel="stylesheet"
            id="hello-elementor-child-style-css"
            href="https://fs.codelinden.com/wp-content/themes/hello-child/style.css?ver=3"
            media="all"
        />
        <link rel="stylesheet" href="header.css" />
        <script src="header.js" defer></script>

        <!-- single-menu dropdown style -->
        <link rel="stylesheet" href="single-menu-dropdown.css" />

        <!-- custom form element style (nice-select) -->
        <link rel="stylesheet" href="form.css" />

        <!-- nice-select style-->
        <link
            rel="stylesheet"
            href="https://cdnjs.cloudflare.com/ajax/libs/jquery-nice-select/1.1.0/css/nice-select.min.css"
            integrity="sha512-CruCP+TD3yXzlvvijET8wV5WxxEh5H8P4cmz0RFbKK6FlZ2sYl3AEsKlLPHbniXKSrDdFewhbmBK5skbdsASbQ=="
            crossorigin="anonymous"
            referrerpolicy="no-referrer"
        />

        <title>Header</title>
    </head>
    <body>
        <header class="header HruDj">
            <div class="nav-dropdown target-id DeYlt">
                <p id="target_id1">toggle dropdown</p>

                <!-- single-menu dropdown container -->
                <div id="menu_id1" class="menu-id">
                    <div class="form-field full-width">
                        <input
                            type="hidden"
                            name="form_my_contact_form"
                            value="1"
                        />
                        <label for="product_type">Product Type</label>
                        <select
                            name="product_type"
                            id="product_type"
                            required=""
                        >
                            <option value="book">Book</option>
                            <option value="movie">Movie</option>
                            <option value="music">Music</option>
                            <option value="" disabled="">
                                Select a product type
                            </option>
                        </select>
                    </div>
                </div>
            </div>
            <div class="nav-dropdown target-id DeYlt">
                <p id="target_id2">toggle dropdown</p>

                <!-- single-menu dropdown container -->
                <div id="menu_id2" class="menu-id">
                    <div class="form-field full-width">
                        <input
                            type="hidden"
                            name="form_my_contact_form"
                            value="1"
                        />
                        <label for="product_type">Product Type</label>
                        <select
                            name="product_type"
                            id="product_type"
                            required=""
                        >
                            <option value="book">Book</option>
                            <option value="movie">Movie</option>
                            <option value="music">Music</option>
                            <option value="" disabled="">
                                Select a product type
                            </option>
                        </select>
                    </div>
                </div>
            </div>
        </div>
        </header>

        <!-- jquery cdn -->
        <script src="popup/jquery/jquery.min.js"></script>

        <!-- single-menu dropdown script -->
        <script src="single-menu-dropdown.js"></script>

        <!-- nice-select script -->
        <script
            src="https://cdnjs.cloudflare.com/ajax/libs/jquery-nice-select/1.1.0/js/jquery.nice-select.min.js"
            integrity="sha512-NqYds8su6jivy1/WLoW8x1tZMRD7/1ZfhWG/jcRQLOzV1k1rIODCpMgoBnar5QXshKJGV7vi0LXLNXPoFsM5Zg=="
            crossorigin="anonymous"
            referrerpolicy="no-referrer"
        ></script>

        <!-- initialize dropdown and select -->
        <script>
            // Call singleMenu function for each menu
           
        </script>
    </body>
</html>

Note: moved the following code to the javascript portion only to make the code snippet runnable, that is not a required modification.

 singleMenu("target_id1", "menu_id1", false);
 singleMenu("target_id2", "menu_id2", false);
 $(document).ready(function () {
   // Apply the niceSelect plugin to all select elements
   $("select").niceSelect();
 });
0
PCDSandwichMan On

I am pretty sure this is just happening because of the event propagation order. When select is clicked, this is what I am seeing is happening:

  1. The click event on select fires the niceSelect function.
  2. The click event then bubbles up to the parent elements, eventually reaching your menuElement and stopping there since you added event.stopPropagation().

I believe the reason this is an issue is because niceSelect does not immediately resolve on click, and it's waiting for the next event loop before it performs its actions. But since the event is hitting stopPropagation() before that ever happens, stopPropagation() stops this from ever happening.

A possibly unorthodox method to fix this would be to just delay your stop on stopPropagation() like so:

menuElement.addEventListener("click", function (event) {
  setTimeout(() => event.stopPropagation(), 0);
});

In layman's terms, this tells javascript "Run this code as soon as the event loop is finished but before the start of the next one".

0
Hao Wu On

When adding event listener to targetElement, try specifying useCapture to true by giving a third property of true. So it triggers before menuElement's click event:

// Toggle menu visibility when target element is clicked
targetElement.addEventListener('click', () => {
    show = !show;

    if (show) {
        // show dropdown
        menuElement.style.display = 'block';
        targetElement.classList.add('active');
    } else {
        // hide dropdown
        menuElement.style.display = 'none';
        targetElement.classList.remove('active');
    }
}, true);  // set useCapture to true