Здравствуй, Хабр!

Хочу поделиться ещё одним способом создания css-перехода (transition) свойства height от 0px до auto.

Столкнулся с данной проблемой при разработке веб-компонентов TreeView и DataGrid. В TreeView решил сделать плавное развёртывание/свёртывание узлов, а в DataGrid — строки с дополнительным контентом. Почитав интернет, нашёл несколько способов реализации, основные — через свойство max-height и на javascript. Реализация на javascript была исключена — есть же css с поддержкой переходов и анимаций. Остался max-height, тем более в примерах с выпадающими меню всё работает.

В TreeView каждый узел имеет неограниченную вложенность, поэтому сразу не получится определить максимальную высоту его содержимого, да и если max-height задать очень большим, будут проблемы с анимацией перехода. Также, если развернуть дочерние узлы, высота родительского увеличится и может перекрыть max-height. Как ни крути, max-height не подходит. С DataGrid та же проблема — дополнительный контент в строке может быть любой. Нужен height:auto!

Итак, приступим к реализации перехода по свойству height от 0px до auto. Рассмотрим простой пример.

Пусть:

  • elBlock: HTMLDivElement — блок, который нужно развёртывать/свёртывать;
  • elToggle: HTMLButtonElement — кнопка-переключатель состояния.

Определим css-класс для блока, в котором установим обрезку содержимого и сам переход:

.block {
    overflow: hidden;
    transition: height 500ms ease;
}

Опишем обработчик события onClick для elToggle:

elToggle.addEventListener("click", () => {
    if (elBlock.style.height === "0px") {
        elBlock.style.height = `${ elBlock.scrollHeight }px`
    } else {
        elBlock.style.height = `${ elBlock.scrollHeight }px`;
        window.getComputedStyle(elBlock, null).getPropertyValue("height");
        elBlock.style.height = "0";
    }
});

Осталось добавить возврат height:auto после перехода:

elBlock.addEventListener("transitionend", () => {
    if (elBlock.style.height !== "0px") {
        elBlock.style.height = "auto"
    }
});

Ну вот и всё, теперь развёртывание/свёртывание блока работает как надо и не зависит от размера контента.


Рисунок 1 — Пример развёртывания/свёртывания узлов в TreeView

Стоит отметить минусы данного подхода:

  • использование javascript, хотелось бы только css;
  • во время перехода может измениться контент (его высота, scrollHeight) и после его завершения, в случае возврата auto, высота блока резко поменяется в ту или иную сторону. Для избежания данного эффекта, необходимо отслеживать изменение scrollHeight и менять height. Как показывает практика, обычно переходы развёртывания/свёртывания занимают по 0.5 с, а за это время пользователь вряд ли успеет изменить что-то внутри, например, в случае TreeView, развернуть дочерний узел.

Спасибо за внимание!

Комментарии (30)


  1. Cerberuser
    12.11.2019 15:07

    Два forced layout на операцию — ну такое… Хотя сильно лучше тут, кажется, не выйдет, да (сам с подобной задачей возился полгода назад).


  1. YNile
    12.11.2019 15:10
    +6

    Реализация на javascript была исключена
    Опишем обработчик события onClick для elToggle:
    image


    1. kingjin Автор
      12.11.2019 15:32
      -1

      Имелась ввиду реализация самой анимации, например, с использованием requestAnimationFrame() для уменьшения/увеличения height, + поддержка timing-functions


  1. vintage
    12.11.2019 17:04

    Лайфхак — можно просто анимировать max-height с ease-in/out в зависимости от направления.


    1. TheShock
      13.11.2019 01:01

      Ну такой себе лайфхак. Тоже требует знания высоты, иначе будет довольно неудачная анимация.


      1. vintage
        13.11.2019 07:49

        Достаточно знать приблизительную высоту для настройки скорости.


        1. TheShock
          13.11.2019 21:31

          Если знать только приблизительную, то вот эту ошибку будет заметно всё-равно. Да и приблизительную тоже надо как-то считать


      1. space2pacman
        16.11.2019 07:22

        Ничего подобного. Делаете max-height 10000px и будет плавная анимация с 0 до высоты блока и обратно. Никаких рывков и прочего. 10к конечно выглядит не супер масштабируемо но вряд ли будет меню с такой высотой.


        1. kingjin Автор
          16.11.2019 07:31

          Подробно, почему так не стоит делать, описано в ветке комментариев https://habr.com/ru/post/475520/#comment_20880230.


  1. monochromer
    12.11.2019 17:16

    Работу с element.style.height === '0px' я б заменил на работу с каким-нибудь классом, например,


    .is-collapsed {
      height: 0;
    }

    А layout-триггеры можно заменить на requestAnimationFrame (но с небольшим кроссбраузерным хаком):


    function onSchedule(fn) {
      /* да, здесь два requestAnimationFrame */     
      requestAnimationFrame(function() {
        requestAnimationFrame(function() {
          fn();
        });
      });
    }

    Полный пример — https://jsfiddle.net/95tnmdyr/


  1. psFitz
    12.11.2019 23:25

    Можно без js с max-height, как написал vintage, гуглитя за 5 сек и статью писать не надо
    https://stackoverflow.com/questions/3508605/how-can-i-transition-height-0-to-height-auto-using-css


    1. Poccomaxa_zt
      13.11.2019 09:42

      Не хочется кидать камни — но ведь автор явно в начале статьи описывал причину его отказа от `max-height`, или я не прав?..
      Конечно, в 90% случаев, думаю, обычного `max-height: 5000px` вполне хватит. Но когда Вы разрабатываете переиспользуемые компоненты с четко описаными требованиями и динамическим контентом — не факт, что Вы не будете в числе 10%.


      1. psFitz
        13.11.2019 11:12

        Пардон, как-то упустил этот момент а статье, каюсь


      1. TheShock
        13.11.2019 21:34

        Конечно, в 90% случаев, думаю, обычного `max-height: 5000px` вполне хватит
        И какое время анимации задавать для такого max-height? Более того, при возвращении max-height:5000 => max-height:0 — будет страшный лаг во время клика. То есть у нашего списка высота, допустим, 250 пикселей. Нажимаем кнопку, оно начинает анимироваться 0.9 секунд з max-height:5000 до max-height:250, а для пользователя это выглядит как тормоза, а потом за 0.1 секунды резко скрывается. Халтура, а не решение


        1. vintage
          13.11.2019 23:41

          Вы зря проигнорировали мою ремарку про анимационные функции.


          1. TheShock
            14.11.2019 14:20

            ease-in/ease-out? Они никак не решают этой проблемы. Может слегка уменьшают. Очень мало уменьшают


            1. vintage
              14.11.2019 15:28

              Во многих случаях достаточно уменьшают.


              1. TheShock
                14.11.2019 17:04

                Та почти во всех случаях они влияют минимально. Более того, невозможно адекватно настроить easing. Для уменьшения много=>ноль при использовании max-height не настраивается ease-in, дл увеличения — ease-out. Выглядит, как результат — отвратительно любому человеку со вкусом


                1. TheShock
                  15.11.2019 21:47
                  +1

                  Мне вот интересны аргументы того, кто минусовал. Использование max-height обрезает или начало (в одну сторону) или конец (в другую). Соответствуенно, функции изинга, которые ответственны за начало и конец — работают некорректно


                  1. vintage
                    16.11.2019 01:19

                    Да всё норм потому что: https://jsbin.com/johobicego/edit?html,css,output


                    1. TheShock
                      16.11.2019 01:30
                      +1

                      И что тут нормового? Они все открываются за разное, неконтролируемое время. ease-out нету ни на открытии, ни на закрытии (конечно, в коде есть, а визуально — нету).

                      Такое сырое решение подойдёт разве что в качестве заглушки: «смотрите, тут потом будет нормальная анимация закрытия, но пока и так сойдёт»


                      1. vintage
                        16.11.2019 12:39

                        И что тут нормового?

                        Можете провести опрос у пользователей и спросить норм или не норм.


                        Они все открываются за разное, неконтролируемое время.

                        Время появления любого элемента напрямую зависит от его позиции. Если вы будете открывать огромный список за то же время, что и маленький, то либо на маленьком будет слишком меееедленная анимация, либо на большом она будет еле заметная.


                        ease-out нету ни на открытии, ни на закрытии (конечно, в коде есть, а визуально — нету).

                        Оно там и не надо. Или вы считаете, что все анимации должны быть ease-in-out?


                        Такое сырое решение подойдёт разве что в качестве заглушки: «смотрите, тут потом будет нормальная анимация закрытия, но пока и так сойдёт»


                        На самом деле единственная реальная, а не вымученная проблема тут — это синхронизация нескольких анимаций соседних элементов. Но она в принципе плохо решается с нелинейными анимациями.


                        1. TheShock
                          16.11.2019 13:23

                          О! Ну раз вы прикольную картинку нашли, то однозначно правы. Ведь подмена тезиса, уход в крайности, сравнивание оппонента с белками-истеричками — это ведь так умно и оригинально (на самом деле нет)

                          Время появления любого элемента напрямую зависит от его позиции
                          Хуже, что оно неконтролируемое.

                          Оно там и не надо. Или вы считаете, что все анимации должны быть ease-in-out?
                          Я считаю, что это должен решать разработчик, а не плохой технический костыль.

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


                          1. vintage
                            16.11.2019 14:52

                            Вы преувеличиваете масштаб трагедии. Из-за чего для реализации простой вещи потратите куда больше времени на написание кода и вылавливание в нём багов. Но никто ваши старания не оценит.


  1. XAHTEP26
    13.11.2019 10:55

    Спасибо, интересное решение. Но весь смысл статьи теряется из-за, того что вы не объясняете зачем тут вообще нужна строка:

    window.getComputedStyle(elBlock, null).getPropertyValue("height");

    Было бы очень полезно если бы вы это описали. Кстати эту строку можно заменить на эту:
    elBlock.clientHeight;


    1. kingjin Автор
      13.11.2019 13:59

      Если блок развёрнут, его height=auto, а для перехода необходимо конкретное значение, которое получаем через scrollHeight. Затем нужно пересчитать («применить») height, для этого используется getComputedStyle() (MDN: getComputedStyle).

      Да, действительно, можно использовать clientHeight, его вызов «применяет» текущее значение height. Благодарю за совет!



      Рисунок — Отличия между offsetHeight, clientHeight и scrollHeight


  1. vital72
    14.11.2019 07:57

    тьфу блин. я думал действительно будет рассказано про переход height от 0 до auto. а тут очевидное решение про от 0 до scrollHeight. ничего нового. зачем обманывать.


  1. onekit
    14.11.2019 12:46

    Никогда не понимал зачем писать рядом с нулём единицы измерения. Если скалярная величина 0, то какая нам разница чего 0. 0px? А если не указывать px, то всё пропало?


    1. kingjin Автор
      14.11.2019 12:55

      Разницы никакой. Если не указать px, сравнение (elBlock.style.height === "0px") не сработает, т.к. при чтении свойства возвращается "0px", даже если перед этим задать ему "0".


  1. Shumanskaya
    16.11.2019 07:19

    Для моих целей помогает добавление к классу .visible — padding-top/padding-bottom. Анимация применяется к ним, создавая плавное раскрытие/скрытие.