Flex transition: Stretch (or shrink) to fit content
It is possible to solve it using max-width
and calc()
.
First, replace width: 100%
with flex: 1
for the divs in CSS, so they will grow, which is better in this case. In addition, use transition for max-width
.
Now, we have to store some relevant values:
- The amount of divs that will be animated (
divsLength
variable) - 3 in this case. - The total width used for the fixed div and the borders (
extraSpace
variable) - 39px in this case.
With those 2 variables, we can set a default max-width
(defaultMaxWidth
variable) to all the divs, as well as using them later. That is why they are being stored globally.
The defaultMaxWidth
is calc((100% - extraSpace)/divsLength)
.
Now, let's enter the click function:
To expand the div, the width of the target text will be stored in a variable called textWidth
and it will be applied to the div as max-width.
It uses .getBoundingClientRect().width
(since it return the floating-point value).
For the remaining divs, it is created a calc()
for max-width
that will be applied to them.
It is: calc(100% - textWidth - extraScape)/(divsLength - 1)
.
The calculated result is the width that each remaining div should be.
When clicking on the expanded div, that is, to return to normal, the default max-width
is applied again to all .div
elements.
var expanded = false,
divs = $(".div:not(:first-child)"),
divsLength = divs.length,
extraSpace = 39, //fixed width + border-right widths
defaultMaxWidth = "calc((100% - " + extraSpace + "px)/" + divsLength + ")";
divs.css("max-width", defaultMaxWidth);
$(document).on("click", ".div:not(:first-child)", function (e) {
var thisInd = $(this).index();
if (expanded !== thisInd) {
var textWidth = $(this).find('span')[0].getBoundingClientRect().width;
var restWidth = "calc((100% - " + textWidth + "px - " + extraSpace + "px)/" + (divsLength - 1) + ")";
//fit clicked fluid div to its content and reset the other fluid divs
$(this).css({ "max-width": textWidth });
$('.div').not(':first').not(this).css({ "max-width": restWidth });
expanded = thisInd;
} else {
//reset all fluid divs
$('.div').not(':first').css("max-width", defaultMaxWidth);
expanded = false;
}
});
.wrapper {
overflow: hidden;
width: 100%;
margin-top: 20px;
border: 1px solid black;
display: flex;
justify-content: flex-start;
}
.div {
overflow: hidden;
white-space: nowrap;
border-right: 1px solid black;
text-align:center;
}
.div:first-child {
min-width: 36px;
background: #999;
}
.div:not(:first-child) {
flex: 1;
transition: max-width 1s;
}
.div:not(:first-child) span {
background: #ddd;
}
.div:last-child {
border-right: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
Click on the div you want to fit/reset (except the first div)
<div class="wrapper">
<div class="div"><span>Fixed</span></div>
<div class="div"><span>Fluid (long long long long text)</span></div>
<div class="div"><span>Fluid</span></div>
<div class="div"><span>Fluid</span></div>
</div>
This approach behaves dynamically and should work on any resolution.
The only value you need to hard code is the extraSpace
variable.
You need to deal with the width or calc functions. Flexbox would have a solution.
To make all divs equal (not first one) we use flex: 1 1 auto
.
<div class="wrapper">
<div class="div"><span>Fixed</span></div>
<div class="div"><span>Fluid (long long long long text)</span></div>
<div class="div"><span>Fluid</span></div>
<div class="div"><span>Fluid</span></div>
</div>
Define flex rules for your normal div and selected div. transition: flex 1s;
is your friend. For selected one we don't need flex grow so we use flex: 0 0 auto
;
.wrapper {
width: 100%;
margin-top: 20px;
border: 1px solid black;
display: flex;
}
.div {
white-space: nowrap;
border-right: 1px solid black;
transition: flex 1s;
flex: 1 1 auto;
}
.div.selected{
flex: 0 0 auto;
}
.div:first-child {
min-width: 50px;
background: #999;
text-align: center;
}
.div:not(:first-child) {
text-align: center;
}
.div:last-child {
border-right: 0px;
}
div:not(:first-child) span {
background: #ddd;
}
Add selected class each time when the user clicks a div. You can also use toggle for the second click so you can save selected items in a map and you can show multiple selected items (not with this code example of course).
$(document).on("click", ".div:not(:first-child)", function(e) {
const expanded = $('.selected');
$(this).addClass("selected");
if (expanded) {
expanded.removeClass("selected");
}
});
https://jsfiddle.net/f3ao8xcj/
After a few trial versions, this seems to be my shortest and most straighforward solution.
All that essentially needs to be done is have Flexbox stretch the <div>
elements to their limits by default, but when <span>
clicked, constraint the stretch of the <div>
to <span>
width ...
pseudo code:
when <span> clicked and already toggled then <div> max-width = 100%, reset <span> toggle state
otherwise <div> max-width = <span> width, set <span> toggle state
I have split the CSS into a 'relevant mechanism' and 'eye-candy only' section for easy reading (and code recyling).
The code is heavily commented, so not much text here...
Quirk Somehow there is an extra delay in the transition
when switching the div
from max-width: 100%
to max-width = span width
. I've checked this behaviour in Chrome, Edge, IE11 and Firefox (all W10) and all seem to have this quirk. Either some browser internal recalc going on, or maybe the transition
time is used twice ('feels like'). Vice Versa, oddly enough, there is no extra delay.
However, with a short transition time (e.g. 150ms, as I am using now) this extra delay is not/hardly noticable. (Nice one for another SO question...)
$(document).on('click', '.wrapper>:not(.caption) span', function (e) {
// Save the current 'toggle' status
var elemToggled = e.target.getAttribute('toggled');
// Set parent max-width to maximum space or constraint to current child width
e.target.parentElement.style.maxWidth =
(elemToggled=="true") ? '100%' : parseFloat(window.getComputedStyle(e.target).width) + 'px';
// (Re)set child toggle state
e.target.setAttribute('toggled', (elemToggled=="true") ? false : true);
});
/*********************/
/* Wrapper mechanism */
/*********************/
.wrapper { /* main flexible parent container */
display : flex; /* [MANDATORY] Flexbox Layout container, can't FBL without */
flex-wrap: nowrap; /* [MANDATORY] default FBL, but important. wrap to next line messes things up */
flex-grow: 1; /* [OPTIONAL] Either: if '.wrapper' is a FBL child itself, allow it to grow */
width : 100%; /* [OPTIONAL] or : full parent width */
/* (Maybe a fixed value, otherwise redundant here as 'flex-grow' = 1) */
}
/* generic rule */
.wrapper>* { /* flexed child containers, being flexible parent containers themselves */
flex-grow : 1; /* [MANDATORY] important for this mechanism to work */
overflow: hidden; /* [MANDATORY] important, otherwise output looks messy */
display: flex; /* [MANDATORY] for FBL stretching */
justify-content: center;/* [MANDATORY] as per SOQ */
max-width : 100%; /* [OPTIONAL/MANDATORY], actually needed to trigger 'transition' */
}
/* exception to the rule */
.wrapper>.fixed { /* fixed child container */
flex-grow: 0; /* [MANDATORY] as per SOQ, don't allow grow */
}
/******************/
/* Eye-candy only */
/******************/
.wrapper {
border: 1px solid black;
}
.wrapper>:not(.fixed) {
transition: max-width 150ms ease-in-out;
}
.wrapper>:not(:last-child){
border-right: 1px solid black;
}
/* generic rule */
.wrapper>*>span {
white-space: nowrap;
background-color: #ddd;
}
/* exception to the rule */
.wrapper>.fixed>span {
background-color: #999;
}
/* debug helper: show all elements with outlines (put in <body>) */
[debug="1"] * { outline: 1px dashed purple }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="wrapper">
<div class="fixed"><span>Fixed</span></div>
<div><span>Fluid (long long long long long text)</span></div>
<div><span>Fluid</span></div>
<div><span>Fluid</span></div>
</div>
UPDATE
New version that resets all other <div>
. I truly hate the jumpiness, but that is due to Flexbox stretching and the transition
value. Without transition
no jumps visible. You need to try out what works for you.
I only added document.querySelectorAll()
to the javascript code.
$(document).on('click', '.wrapper>:not(.caption) span', function (e) {
var elemToggled = e.target.getAttribute('toggled'); // Toggle status
var elemWidth = parseFloat(window.getComputedStyle(e.target).width); // Current element width
// reset ALL toggles but 'this'...
document.querySelectorAll('.wrapper>:not(.caption) span')
.forEach( function (elem,idx) {
if (elem != this){
elem.parentElement.style.maxWidth = '100%';
elem.setAttribute('toggled',false);
};
});
// Set parent max-width to maximum space or constraint to current child width
e.target.parentElement.style.maxWidth =
(elemToggled=="true") ? '100%' : parseFloat(window.getComputedStyle(e.target).width) + 'px';
// (Re)set child toggle state
e.target.setAttribute('toggled', (elemToggled=="true") ? false : true);
});
/*********************/
/* Wrapper mechanism */
/*********************/
.wrapper { /* main flexible parent container */
display : flex; /* [MANDATORY] Flexbox Layout container, can't FBL without */
flex-wrap: nowrap; /* [MANDATORY] default FBL, but important. wrap to next line messes things up */
flex-grow: 1; /* [OPTIONAL] Either: if '.wrapper' is a FBL child itself, allow it to grow */
width : 100%; /* [OPTIONAL] or : full parent width */
/* (Maybe a fixed value, otherwise redundant here as 'flex-grow' = 1) */
}
/* generic rule */
.wrapper>* { /* flexed child containers, being flexible parent containers themselves */
flex-grow : 1; /* [MANDATORY] important for this mechanism to work */
overflow: hidden; /* [MANDATORY] important, otherwise output looks messy */
display: flex; /* [MANDATORY] for FBL stretching */
justify-content: center;/* [MANDATORY] as per SOQ */
max-width : 100%; /* [OPTIONAL/MANDATORY], actually needed to trigger 'transition' */
}
/* exception to the rule */
.wrapper>.fixed { /* fixed child container */
flex-grow: 0; /* [MANDATORY] as per SOQ, don't allow grow */
}
/******************/
/* Eye-candy only */
/******************/
.wrapper {
border: 1px solid black;
}
.wrapper>:not(.fixed) {
transition: max-width 150ms ease-in-out;
}
.wrapper>:not(:last-child){
border-right: 1px solid black;
}
/* generic rule */
.wrapper>*>span {
white-space: nowrap;
background-color: #ddd;
}
/* exception to the rule */
.wrapper>.fixed>span {
background-color: #999;
}
/* show all elements with outlines (put in <body>) */
[debug="1"] * { outline: 1px dashed purple }
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="wrapper">
<div class="fixed"><span>Fixed</span></div>
<div><span>Fluid (long long long long long text)</span></div>
<div><span>Fluid</span></div>
<div><span>Fluid</span></div>
</div>