Home > Back-end >  animating a menu opening to the proper height
animating a menu opening to the proper height

Time:12-31

I have a function that is meant to animate opening of a ul based navigation menu. unfortunately it just snaps into place instead of animating like it should. Anyone know how to fix this?

function transitionComplete(event) {
    event.target.style.removeProperty("height")
    event.target.removeEventListener('transitionend', transitionComplete)
}

function openNavMenu() {
    let nav = document.querySelector("#main-nav")
    let menus = nav.querySelectorAll(".menus")

    for (let menu of menus) {
        // disable any modified height
        menu.style.height = 'initial'

        //measure height
        let expandedHeight = menu.offsetHeight

        // set the height back to zero in prep for animation.
        menu.style.height = '0px';

        menu.addEventListener('transitionend', transitionComplete, true)

 // HERE - I cannot figure out what I am supposed to do here
        // menu.style.height = expandedHeight
        requestAnimationFrame(() => {
            menu.style.height = expandedHeight   "px"
        })

    }
    nav.setAttribute(expanded, "true")
}

Here is the complete code including the script above, css and html:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        nav ul {
            list-style: none;
        }

        #main-nav {
            width: 80vw;
            background-color: #0000000A;
            display: flex;
            padding: 10px 0;
            margin: 20px auto;
        }

        #main-nav .menus {
            flex: 1;
            overflow: hidden;
            margin-left: max(0px, (100vw - 300px) / 4);
            padding-left: 20px;
            transition: 1s ease height;
        }

        #main-nav .menus li a {
            text-decoration: none;
            font-family: "Crimson Text", serif;
            letter-spacing: 2px;
            padding: 5px 5px 10px 0;
            margin: 5px 0;
            display: block;
        }

        #main-nav .menus > li > a {
            color: #7c6949;
        }

        #main-nav .menus > li ul {
            padding-left: 20px;
        }

        #main-nav .menus > li ul li:not(:last-child) a {
            border-bottom: 1px solid #0000000A;
        }

        #main-nav .control {
            background: none;
            border: none;
            display: flex;
            margin-right: 10px;
            background: orange;
            padding: 10px;
        }

        #main-nav[expanded] .control.open {
            display: none;
        }

        #main-nav:not([expanded]) .menus {
            height: 0;
        }

        #main-nav:not([expanded]) .control.close {
            display: none;
        }


        #footer-nav > .menus {
            letter-spacing: 2px;
            padding: 0;
            margin: 0;
        }

        #footer-nav > .menus > li ul {
            padding: 0;
            margin: 0;
            display: flex;
            flex-direction: column;
        }

        #footer-nav > .menus > li ul li {
            display: inline-block;
        }

        #footer-nav > .menus > li ul li a {
            text-decoration: none;
            padding: 3px 0;
            text-transform: uppercase;
            display: inline-block;
            margin: 6px 0 0;
        }

        #footer-nav > .menus > li > a {
            display: none;
        }
    </style>
</head>
<body>

<nav id="main-nav">
    <ul >
        <li>
            <a href="#">About</a>
            <ul>
                <li><a href="/">Biographies</a></li>
                <li><a href="/">Testimonials</a></li>
                <li><a href="/">FAQ</a></li>
            </ul>
        </li>
        <li>
            <a href="#">Subscribe</a>
            <ul>
                <li><a href="/">Subscribe</a></li>
                <li><a href="/">Renew</a></li>
            </ul>
        </li>
        <li>
            <a href="#">Contact</a>
            <ul>
                <li><a href="/">Contact Us</a></li>
            </ul>
        </li>
    </ul>
    <div >
        <button  onclick="openNavMenu()">
            open
        </button>
        <button  onclick="closeNavMenu()">
            close
        </button>
    </div>
</nav>

<script>
    const expanded = "expanded"

    function transitionComplete(event) {
        event.target.style.removeProperty("height")
        event.target.removeEventListener('transitionend', transitionComplete)
    }

    function openNavMenu() {
        let nav = document.querySelector("#main-nav")
        let menus = nav.querySelectorAll(".menus")

        for (let menu of menus) {
            // disable any modified height
            menu.style.height = 'initial'

            //measure height
            let expandedHeight = menu.offsetHeight

            // replace disabled styles
            menu.style.height = '0px';

            menu.addEventListener('transitionend', transitionComplete, true)

// HERE - What do I do?
            // menu.style.height = expandedHeight
            requestAnimationFrame(() => {
                menu.style.height = expandedHeight   "px"
            })

        }
        nav.setAttribute(expanded, "true")
    }

    function closeNavMenu() {
        let nav = document.querySelector("#main-nav")
        let menus = nav.querySelectorAll(".menus")
        nav.removeAttribute(expanded)
        for (let menu of menus) {
            menu.style.removeProperty("height");
        }
    }

</script>

</body>
</html>

CodePudding user response:

You can't animate from a property that has a value to a property value that isn't set.

Meaning, menu.style.removeProperty("height") this line removes the property and there for it has no value. By setting the height to 0 it will animate back to that.

function closeNavMenu() {
  let nav = document.querySelector("#main-nav");
  let menus = nav.querySelectorAll(".menus");
  nav.removeAttribute(expanded);
  for (let menu of menus) {
    menu.style.height = "0px"
  }
}

Another topic:
There are probably opinions about this but I believe the majority of the industry do like this (also linters are setup default like it) but don't use let use const, use let for values you want to override and one example could be a for loop where you want to increment the i, const will assure you value won't get overridden.

CodePudding user response:

Interesting...

  • in a chromimum browser (Chrome and Edge on my windows PC and Chrome on my Android phone) the animation works when opening the menu, but there is no animation when closing
  • when viewed in FireFox the result is reversed, the closing animation works and the opening one doesn't

Chrome, I think, is suffering from the lack of a defined height at the start of the closing animation, as mentioned by @Dejan.S

Removing the event.target.style.removeProperty("height", event.target) from the transitionComplete callback fixes that issue

Unfortunately the requestAnimationFrame doesn't seem to have the desired effect in FireFox, but the old faithful trick of setting a zero timeout seems to work

setTimeout(() => {
                    menu.style.height = expandedHeight   "px"
            }, 0)
  • Related