Next.js Discord

Discord Forum

Handling State with Height for Dropdown Menu

Unanswered
vTotal posted this in #help-forum
Open in Discord
I'm currently working on a project and am looking to build an animated dropdown menu, one where the height of the nav container animates depending on the height of the inner content.
I've gotten it to a point where it handles changing heights, but it doesn't quite do so correctly and I can't figure out how to solve it.

When I open a sub nav item, the height of the nav container instantly changes. So lets say I click on a sub nav item, it'll take the height that it starts at (0px) and adjust the nav container height by that much. But if I then click again on the sub nav item to close it, it'll take the height again (24px) and adjust the height of the container by that much, even as the sub nav item is closed and the value is reduced to 0. I'm just struggling to figure out how to handle the state and the height in a way that I can get the container height to calculate properly.

Code posted below:

4 Replies

Nav Container code:

import NavList from './navList';
import { useState, useRef, useEffect, useContext } from 'react';
import { MenuContext } from './menuContext';

export const NavContainer = ({ items, toggle, menuOpen }) => {
    const [navHeight, setNavHeight] = useState(0);
    const navRef = useRef(null);
    const { submenus, toggleSubmenu } = useContext(MenuContext);

    useEffect(() => {
        if (menuOpen && Object.values(submenus).some(isOpen => isOpen)) {
            setHeight();
        } else if (!menuOpen) {
            setNavHeight(0);
            Object.keys(submenus).forEach(id => {
                if (submenus[id]) {
                    toggleSubmenu(id);
                }
            });
        } else {
            setHeight();
        }
    }, [menuOpen, submenus]);

    const handleMenuClick = () => {
        toggle();
    };

    const setHeight = () => {
        const innerHeight = navRef.current.firstChild.getBoundingClientRect().height;
        setNavHeight(innerHeight);
    };

    return (
        <>
            <button onClick={handleMenuClick} title={menuOpen ? 'Close Menu' : 'Open Menu'} >
                <svg xmlns="http://www.w3.org/2000/svg"
                    viewBox="0 0 24 24"
                    fill="none"
                    stroke="currentColor"
                    strokeWidth="2"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                    width={20}
                    role='presentation'
                    >
                    <line x1="3" y1="12" x2="21" y2="12"></line>
                    <line x1="3" y1="6" x2="21" y2="6"></line>
                    <line x1="3" y1="18" x2="21" y2="18"></line>
                </svg>
            </button>
            <nav className={`overflow-hidden transition-all`} style={{height: navHeight}} ref={navRef}>
                <NavList items={items} />
            </nav>
        </>
    )
};

export default NavContainer;
Nav List code:

import NavItem from './navItem';

export const NavList = ({ items }) => {

    return (
        <ul>
            {items.map((item, index) => (
                <NavItem key={index} item={item} />
            ))}
        </ul>
    )
};

export default NavList;
Nav Item code

import Link from 'next/link';
import SubNavList from './subNavList';

export const NavItem = ({ item }) => {
    const isSubItem = item.childItems.edges.length > 0;

    return (
        <li key={item.id}>
            <Link href={item.uri}>
                {item.label}
            </Link>
            {isSubItem && (
                <SubNavList id={item.id} items={item.childItems.edges.map(edge => edge.node)} />
            )}
        </li>
    )
};

export default NavItem;
Sub Nav List code:

import NavItem from './navItem';
import { useState, useRef, useContext, useEffect } from 'react';
import { MenuContext } from './menuContext';

export const SubNavList = ({ id, items }) => {
    const { submenus, toggleSubmenu } = useContext(MenuContext);
    const isSubmenuOpen = submenus[id] || false;
    const submenuRef = useRef(null);
    const [submenuHeight, setSubmenuHeight] = useState(0);

    useEffect(() => {
        if (isSubmenuOpen) {
            setHeight();
        } else {
            setSubmenuHeight(0);
        }
    }, [submenus, isSubmenuOpen,]);

    const setHeight = () => {
        const innerHeight = submenuRef.current.firstChild.getBoundingClientRect().height;
        setSubmenuHeight(innerHeight);
    };

    return (
        <>
            <button onClick={() => toggleSubmenu(id)} title={isSubmenuOpen ? 'Close Sub Menu' : 'Open Sub Menu'}>
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width={20}>
                    <path d="M12 15l-6-6h12z" />
                </svg>
            </button>
            <ul className={`overflow-hidden transition-[height]`} ref={submenuRef} style={{height: submenuHeight}}>
                {items.map((item, index) => (
                    <NavItem key={index} item={item} />
                ))}
            </ul>
        </>
    )
};

export default SubNavList;