Я знаю, что это тридцать третий ответ на этот вопрос, но я думаю, что оно того стоит, так что здесь. Это решение только для CSS со следующими свойствами:
- В начале отсрочки нет, и переход не прекращается рано. В обоих направлениях (развертывание и свертывание), если в CSS указать продолжительность перехода 300 мс, переход займет 300 мс (точка).
- Он изменяет фактическую высоту (в отличие от
transform: scaleY(0)
), поэтому он правильно работает, если после свертываемого элемента есть контент.
- Хотя (как и в других решениях) являются магическими числами (например, «выберите длину, превышающую вашу коробку»), это не смертельно, если ваше предположение окажется неверным , В этом случае переход может не выглядеть удивительно, но до и после перехода это не проблема: в расширенном состоянии (
height: auto
) все содержимое всегда имеет правильную высоту (в отличие, например, если вы выбрали max-height
это оказывается слишком низким). А в свернутом состоянии высота равна нулю, как и должно быть.
Демо
Вот демонстрация с тремя складными элементами разной высоты, которые используют один и тот же CSS. Возможно, вы захотите нажать «Полная страница» после нажатия «Запустить фрагмент». Обратите внимание, что JavaScript переключает только CSS-класс collapsed
, измерение не требуется. (Вы можете сделать эту точную демонстрацию вообще без JavaScript, используя флажок или :target
). Также обратите внимание, что часть CSS, которая отвечает за переход, довольно коротка, и HTML требует только одного дополнительного элемента-оболочки.
$(function () {
$(".toggler").click(function () {
$(this).next().toggleClass("collapsed");
$(this).toggleClass("toggled"); // this just rotates the expander arrow
});
});
.collapsible-wrapper {
display: flex;
overflow: hidden;
}
.collapsible-wrapper:after {
content: '';
height: 50px;
transition: height 0.3s linear, max-height 0s 0.3s linear;
max-height: 0px;
}
.collapsible {
transition: margin-bottom 0.3s cubic-bezier(0, 0, 0, 1);
margin-bottom: 0;
max-height: 1000000px;
}
.collapsible-wrapper.collapsed > .collapsible {
margin-bottom: -2000px;
transition: margin-bottom 0.3s cubic-bezier(1, 0, 1, 1),
visibility 0s 0.3s, max-height 0s 0.3s;
visibility: hidden;
max-height: 0;
}
.collapsible-wrapper.collapsed:after
{
height: 0;
transition: height 0.3s linear;
max-height: 50px;
}
/* END of the collapsible implementation; the stuff below
is just styling for this demo */
#container {
display: flex;
align-items: flex-start;
max-width: 1000px;
margin: 0 auto;
}
.menu {
border: 1px solid #ccc;
box-shadow: 0 1px 3px rgba(0,0,0,0.5);
margin: 20px;
}
.menu-item {
display: block;
background: linear-gradient(to bottom, #fff 0%,#eee 100%);
margin: 0;
padding: 1em;
line-height: 1.3;
}
.collapsible .menu-item {
border-left: 2px solid #888;
border-right: 2px solid #888;
background: linear-gradient(to bottom, #eee 0%,#ddd 100%);
}
.menu-item.toggler {
background: linear-gradient(to bottom, #aaa 0%,#888 100%);
color: white;
cursor: pointer;
}
.menu-item.toggler:before {
content: '';
display: block;
border-left: 8px solid white;
border-top: 8px solid transparent;
border-bottom: 8px solid transparent;
width: 0;
height: 0;
float: right;
transition: transform 0.3s ease-out;
}
.menu-item.toggler.toggled:before {
transform: rotate(90deg);
}
body { font-family: sans-serif; font-size: 14px; }
*, *:after {
box-sizing: border-box;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="container">
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
<div class="menu">
<div class="menu-item">Something involving a holodeck</div>
<div class="menu-item">Send an away team</div>
<div class="menu-item toggler">Advanced solutions</div>
<div class="collapsible-wrapper collapsed">
<div class="collapsible">
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
<div class="menu-item">Separate saucer</div>
<div class="menu-item">Send an away team that includes the captain (despite Riker's protest)</div>
<div class="menu-item">Ask Worf</div>
<div class="menu-item">Something involving Wesley, the 19th century, and a holodeck</div>
<div class="menu-item">Ask Q for help</div>
</div>
</div>
<div class="menu-item">Sweet-talk the alien aggressor</div>
<div class="menu-item">Re-route power from auxiliary systems</div>
</div>
</div>
Как это работает?
На самом деле два перехода вовлечены в это. Один из них переводит margin-bottom
с 0px (в развернутом состоянии) на -2000px
в свернутом состоянии (аналогично этому ответу ). 2000 здесь - первое магическое число, оно основано на предположении, что ваша коробка не будет выше этой (2000 пикселей кажется разумным выбором).
Использование одного перехода margin-bottom
само по себе имеет две проблемы:
- Если у вас на самом деле есть ящик, который превышает 2000 пикселей, то
margin-bottom: -2000px
не будет скрывать все - будут видимые вещи даже в свернутом корпусе. Это небольшое исправление, которое мы сделаем позже.
- Если фактическое поле имеет, скажем, высоту 1000 пикселей, а ваш переход имеет длину 300 мс, то переход видимый уже завершен примерно через 150 мс (или, в противоположном направлении, начинается с опозданием на 150 мс) .
Исправление этой второй проблемы - это то, где вступает второй переход, и этот переход концептуально нацелен на минимальную высоту оболочки («концептуально», потому что мы на самом деле не используем свойство min-height
для этого; больше об этом позже).
Вот анимация, которая показывает, как объединение перехода нижнего поля с переходом минимальной высоты, одинаковой длительности, дает нам комбинированный переход от полной высоты к нулевой высоте, которая имеет одинаковую длительность.
Левая полоса показывает, как отрицательное нижнее поле сдвигает нижнюю часть вверх, уменьшая видимую высоту. Средняя полоса показывает, как минимальная высота гарантирует, что в случае свертывания переход не заканчивается досрочно, а в случае растяжения переход не начинается поздно. Правая полоса показывает, как комбинация этих двух элементов приводит к тому, что окно переходит от полной высоты к нулевой высоте за правильное время.
Для моей демонстрации я установил 50px в качестве верхнего минимального значения высоты.Это второе магическое число, и оно должно быть ниже, чем высота коробки.50px кажется разумным;кажется маловероятным, что вам очень часто захочется сделать разборный элемент, высота которого даже не достигает 50 пикселей.
Как видно из анимации, результирующий переход будет непрерывным, ноне дифференцируемо - в момент, когда минимальная высота равна полной высоте, отрегулированной нижним полем, происходит внезапное изменение скорости.Это очень заметно в анимации, потому что она использует линейную функцию синхронизации для обоих переходов, и потому что весь переход очень медленный.В реальном случае (моя демонстрация вверху) переход занимает всего 300 мс, а переход нижнего поля не является линейным.Я поиграл с множеством различных функций синхронизации для обоих переходов, и те, с которыми я закончил, чувствовали, что они лучше всего работают в самых разных случаях.
Осталось решить две проблемы:
- точка сверху, где прямоугольники высотой более 2000 пикселей не полностью скрыты в свернутом состоянии,
- и обратная проблема, где в не скрытом случае прямоугольникивысота менее 50 пикселей слишком высока, даже когда переход не выполняется, потому что минимальная высота сохраняет их на уровне 50 пикселей.
Первую проблему мы решаем, задав элементу контейнера max-height: 0
в свернутом случае с переходом 0s 0.3s
.Это означает, что на самом деле это не переход, но max-height
применяется с задержкой;он применяется только после завершения перехода.Чтобы это работало правильно, нам также нужно выбрать числовое значение max-height
для противоположного, не свернутого состояния.Но в отличие от случая 2000px, где выбор слишком большого числа влияет на качество перехода, в этом случае это действительно не имеет значения.Таким образом, мы можем просто выбрать число, которое настолько высоко, что мы знаем , что никакая высота никогда не приблизится к этому.Я выбрал миллион пикселей.Если вам кажется, что вам может потребоваться поддержка контента высотой более миллиона пикселей, то 1) извините и 2) просто добавьте пару нулей.
Вторая проблема заключается в причине, по котороймы на самом деле не используем min-height
для минимальной высоты перехода.Вместо этого в контейнере есть ::after
псевдоэлемент с height
, который переходит от 50px к нулю.Это имеет тот же эффект, что и min-height
: он не позволит контейнеру сжиматься ниже той высоты, которую в настоящее время имеет псевдоэлемент.Но поскольку мы используем height
, а не min-height
, теперь мы можем использовать max-height
(еще раз применен с задержкой), чтобы установить фактическую высоту псевдоэлемента в ноль после завершения перехода, гарантируя, что по крайней меревне перехода даже маленькие элементы имеют правильную высоту.Поскольку min-height
на сильнее , чем max-height
, это не сработало бы, если бы мы использовали контейнер min-height
вместо псевдоэлемента height
.Как и max-height
в предыдущем абзаце, этому max-height
также необходимо значение для противоположного конца перехода.Но в этом случае мы можем просто выбрать 50px.
Протестировано в Chrome (Win, Mac, Android, iOS), Firefox (Win, Mac, Android), Edge, IE11 (за исключением проблемы макета flexbox смое демо, которое я не потрудился отлаживать), и Safari (Mac, iOS).Говоря о flexbox, должна быть возможность сделать это без использования flexbox;на самом деле, я думаю, что вы можете заставить почти все работать в IE7 - за исключением того факта, что у вас не будет переходов CSS, что делает это довольно бессмысленным упражнением.