Как сделать плавным раскрытие вертикального многоуровневого меню?

Есть вертикальное меню-каталог с неограниченным уровнем вложенности. Я столкнулся с проблемой - не могу анимировать его раскрытие и закрытие. Свойство display не анимируется, height тоже. Есть ещё вариант с max-height, но всё это слишком громоздко получается.

Вот пример, все пункты Sub имеют вложенное подменю.

document.querySelector('.root-nav').onclick = function(event) {

    if (event.target.nodeName !== 'SPAN') return

    closeAllSubMenu(event.target.nextElementSibling)
    event.target.classList.add('submenu-active-span')
    event.target.nextElementSibling.classList.toggle('submenu-active')

}

function closeAllSubMenu(currentMenu = null) {
    const parents = []

    if (currentMenu) {
        let currentParent = currentMenu.parentNode
        while(currentParent) {
            if (currentParent.classList.contains('root-nav')) break
            if (currentParent.nodeName === 'UL') parents.push(currentParent)
            currentParent = currentParent.parentNode
        }
    }

    const allSubMenu = document.querySelectorAll('.root-nav ul')
    Array.from(allSubMenu).forEach(item => {
        if (item !== currentMenu && !parents.includes(item)) {
            item.classList.remove('submenu-active')
            if (item.previousElementSibling.nodeName === 'SPAN') {
                item.previousElementSibling.classList.remove('submenu-active-span')
            }
        }
    })
}
* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}
.root-nav {
    width: 300px;
}

.root-nav li {
    list-style-type: none;
    background-color: coral;
    padding-left: 0;
    position: relative;
}

.root-nav a, .root-nav span {
    text-decoration: none;
    color: white;
    display: block;
    padding: 5px 10px;
    cursor: pointer;
}

.root-nav ul {
    display: none;
    width: 100%;
}

ul.submenu-active {
    display: block;
    padding-left: 20px;
}

span.submenu-active-span {
    background-color: crimson;
}
    <ul class="root-nav">
        <li><a href="#">Link 1</a></li>
        <li>
            <span>Sub 1</span>
            <ul>
                <li><a href="#">Link 10</a></li>
                <li><a href="#">Link 20</a></li>
                <li><a href="#">Link 30</a></li>
                <li><a href="#">Link 40</a></li>
            </ul>
        </li>
        <li><a href="#">Link 3</a></li>
        <li><a href="#">Link 4</a></li>
        <li>
            <span>Sub 2</span>
            <ul>
                <li><a href="#">Link 50</a></li>
                <li><a href="#">Link 60</a></li>
                <li>
                    <span>Sub 20</span>
                    <ul>
                        <li><a href="#">Link 100</a></li>
                        <li><a href="#">Link 200</a></li>
                        <li>
                            <span>Sub 3</span>
                            <ul>
                                <li><a href="#">Link 1000</a></li>
                                <li><a href="#">Link 2000</a></li>
                                <li><a href="#">Link 3000</a></li>
                                <li><a href="#">Link 4000</a></li>
                            </ul>
                        </li>
                        </li>
                        <li><a href="#">Link 400</a></li>
                    </ul>
                </li>
                <li><a href="#">Link 80</a></li>
            </ul>
        </li>
    </ul>

Может, знает кто как сделать плавное раскрытие вертикального меню с неограниченной вложенностью?


Ответы (1 шт):

Автор решения: Alexander Kiselev

Можно анимировать с помощью max-height + overflow: hidden.

Получится давольно красиво разворачивать подменю.

document.querySelector('.root-nav').onclick = function(event) {

    if (event.target.nodeName !== 'SPAN') return

    closeAllSubMenu(event.target.nextElementSibling)
    event.target.classList.add('submenu-active-span')
    event.target.nextElementSibling.classList.toggle('submenu-active')

}

function closeAllSubMenu(currentMenu = null) {
    const parents = []

    if (currentMenu) {
        let currentParent = currentMenu.parentNode
        while(currentParent) {
            if (currentParent.classList.contains('root-nav')) break
            if (currentParent.nodeName === 'UL') parents.push(currentParent)
            currentParent = currentParent.parentNode
        }
    }

    const allSubMenu = document.querySelectorAll('.root-nav ul')
    Array.from(allSubMenu).forEach(item => {
        if (item !== currentMenu && !parents.includes(item)) {
            item.classList.remove('submenu-active')
            if (item.previousElementSibling.nodeName === 'SPAN') {
                item.previousElementSibling.classList.remove('submenu-active-span')
            }
        }
    })
}
* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}
.root-nav {
    width: 300px;
}

.root-nav li {
    list-style-type: none;
    background-color: coral;
    padding-left: 0;
    position: relative;
    border: 1px solid;
}

.root-nav a, .root-nav span {
    text-decoration: none;
    color: white;
    display: block;
    padding: 5px 10px;
    cursor: pointer;
}

.root-nav ul {
    height: 100%;
    width: 100%;
    padding-left: 20px;
    max-height: 0px; /* <-- Устанавливаем по умолчанию максимальную высоту 0 */
    transition: max-height 0.15s ease-out; /* <-- Добавляем плавность изменения */
    overflow: hidden; /* <-- скрываем внутренний контент */
}


span.submenu-active-span {
    background-color: crimson;
}

span.submenu-active-span+ul.submenu-active {
    max-height: 500px; /* <-- раскрываем блок */
    transition: max-height 0.25s ease-in; /* <-- добавляем правила плавности для закрытия меню */
}
<ul class="root-nav">
        <li><a href="#">Link 1</a></li>
        <li>
            <span>Sub 1</span>
            <ul>
                <li><a href="#">Link 10</a></li>
                <li><a href="#">Link 20</a></li>
                <li><a href="#">Link 30</a></li>
                <li><a href="#">Link 40</a></li>
            </ul>
        </li>
        <li><a href="#">Link 3</a></li>
        <li><a href="#">Link 4</a></li>
        <li>
            <span>Sub 2</span>
            <ul>
                <li><a href="#">Link 50</a></li>
                <li><a href="#">Link 60</a></li>
                <li>
                    <span>Sub 20</span>
                    <ul>
                        <li><a href="#">Link 100</a></li>
                        <li><a href="#">Link 200</a></li>
                        <li>
                            <span>Sub 3</span>
                            <ul>
                                <li><a href="#">Link 1000</a></li>
                                <li><a href="#">Link 2000</a></li>
                                <li><a href="#">Link 3000</a></li>
                                <li><a href="#">Link 4000</a></li>
                            </ul>
                        </li>
                        </li>
                        <li><a href="#">Link 400</a></li>
                    </ul>
                </li>
                <li><a href="#">Link 80</a></li>
            </ul>
        </li>
    </ul>

→ Ссылка