Bob
Bob

Reputation: 16623

How can I make a descendent element of a flexbox item scrollable?

I am using a flexbox layout with the goal of expanding the last item to fit the available screen space. When that item is too large, I would like to scroll one of the descendant elements. However, I'm unable to get scrolling working as desired.

Example layout:

var node = document.getElementById("table-body");
for (var i = 0; i < 200; i++) {
  node.innerHTML += '<tr><td>' + i + '</td></tr>';
}
html,
body {
  height: 100%;
  margin: 0px;
}
.flexbox {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
#grid-selected-container {
  flex-basis: 30%;
  flex-grow: 0;
  flex-shrink: 0;
  align-self: stretch;
}
#grid-available-container {
  flex-grow: 1;
  flex-shrink: 1;
  align-self: stretch;
  height: 100%;
}
<div class="flexbox">
  <div id="grid-selected-container">
    <table width="100%" class="display" id="grid-selected">
      <tr>
        <td>placeholder</td>
      </tr>
    </table>
  </div>
  <button id="button-render" type="button" class="btn btn-default btn-lg">placeholder</button>
  <div id="grid-available-container">
    <div class="scroll-wrapper">
      <div class="fixed-head">
        <table width="100%" class="display">
          <thead>
            <th>header</th>
          </thead>
        </table>
      </div>
      <div class="scroll-body">
        <table width="100%" class="display" id="grid-available">
          <tbody id="table-body">
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

I can scroll the entire flex item:

var node = document.getElementById("table-body");
for (var i = 0; i < 200; i++) {
  node.innerHTML += '<tr><td>' + i + '</td></tr>';
}
html,
body {
  height: 100%;
  margin: 0px;
}
.flexbox {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
#grid-selected-container {
  flex-basis: 30%;
  flex-grow: 0;
  flex-shrink: 0;
  align-self: stretch;
}
#grid-available-container {
  flex-grow: 1;
  flex-shrink: 1;
  align-self: stretch;
  height: 100%;
  overflow: auto;
}
<div class="flexbox">
  <div id="grid-selected-container">
    <table width="100%" class="display" id="grid-selected">
      <tr>
        <td>placeholder</td>
      </tr>
    </table>
  </div>
  <button id="button-render" type="button" class="btn btn-default btn-lg">placeholder</button>
  <div id="grid-available-container">
    <div class="scroll-wrapper">
      <div class="fixed-head">
        <table width="100%" class="display">
          <thead>
            <th>header</th>
          </thead>
        </table>
      </div>
      <div class="scroll-body">
        <table width="100%" class="display" id="grid-available">
          <tbody id="table-body">
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

But I don't want to scroll the entire flex item. I want to scroll the table body, i.e. the scroll-body div. fixed-head, which contains the table header amongst other things, should not scroll out of view. This is where I run into issues:

var node = document.getElementById("table-body");
for (var i = 0; i < 200; i++) {
  node.innerHTML += '<tr><td>' + i + '</td></tr>';
}
html,
body {
  height: 100%;
  margin: 0px;
}
.flexbox {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
#grid-selected-container {
  flex-basis: 30%;
  flex-grow: 0;
  flex-shrink: 0;
  align-self: stretch;
}
#grid-available-container {
  flex-grow: 1;
  flex-shrink: 1;
  align-self: stretch;
  height: 100%;
}
.scroll-wrapper {
  height: 100%;
}
.scroll-body {
  height: 100%;
  overflow: auto;
}
<div class="flexbox">
  <div id="grid-selected-container">
    <table width="100%" class="display" id="grid-selected">
      <tr>
        <td>placeholder</td>
      </tr>
    </table>
  </div>
  <button id="button-render" type="button" class="btn btn-default btn-lg">placeholder</button>
  <div id="grid-available-container">
    <div class="scroll-wrapper">
      <div class="fixed-head">
        <table width="100%" class="display">
          <thead>
            <th>header</th>
          </thead>
        </table>
      </div>
      <div class="scroll-body">
        <table width="100%" class="display" id="grid-available">
          <tbody id="table-body">
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

To allow scrolling (overflow: auto) at all, I need to set a fixed height on the parents - apparently the flexbox isn't enough. But when I set a fixed height of 100%, the flex item expands to the size of the entire screen, instead of shrinking. I want the main body to remain unscrollable, so the flex item should shrink.

How can I make the scroll-body div scroll while preventing scrolling elsewhere?


The real table is rendered using the jQuery DataTables plugin - the code in this question is merely an attempt to replicate the structure and reproduce the problem.

Upvotes: 1

Views: 631

Answers (1)

Bob
Bob

Reputation: 16623

Note: as of June 2015, this works on Firefox and IE (!) but fails on Chrome. Possibly this Chrome flexbox bug.

A combination of nested flex layouts and the absolute positioned scrollable box trick humble.rumble suggested:

var node = document.getElementById("table-body");
for (var i = 0; i < 200; i++) {
  node.innerHTML += '<tr><td>' + i + '</td></tr>';
}
html,
body {
  height: 100%;
  margin: 0px;
}
.flexbox {
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}
#grid-selected-container {
  flex-basis: 30%;
  flex-grow: 0;
  flex-shrink: 0;
  align-self: stretch;
}
#grid-available-container {
  flex-grow: 1;
  flex-shrink: 1;
  align-self: stretch;
}
.scroll-wrapper {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  height: 100%;
}
.scroll-body {
  position: relative;
  height: 100%;
}
.scroll-body tbody {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  overflow: auto;
}
<div class="flexbox">
  <div id="grid-selected-container">
    <table width="100%" class="display" id="grid-selected">
      <tr>
        <td>placeholder</td>
      </tr>
    </table>
  </div>
  <button id="button-render" type="button" class="btn btn-default btn-lg">placeholder</button>
  <div id="grid-available-container">
    <div class="scroll-wrapper">
      <div class="fixed-head">
        <table width="100%" class="display" id="grid-available">
          <thead>
            <th>header</th>
          </thead>
        </table>
      </div>
      <div class="scroll-body">
        <table width="100%" class="display" id="grid-available">
          <tbody id="table-body">
          </tbody>
        </table>
      </div>
    </div>
  </div>
</div>

There are three layers. First, a flex layout (.scroll-wrapper) with the table header and body as its children. The flex layout will take care of letting the fixed header take as much space as it wants, and filling the rest with the body.

Then the actual scrollable area is two block-layout divs (in this case, a div and a tbody). The first one (.scroll-body) is the relatively-positioned block that fills the rest of the flex, and the second level (.scroll-body tbody) is the absolutely-positioned block where the scrollable content goes - the absolute positioning prevents it from expanding the first level.

Upvotes: 1

Related Questions