5

I am working on a project where I will be injecting markup into pages I don't have control over (an embed script). An interesting case has come up where my injected div is contained within a table. A child of my div is a horizontal menu that should scroll (overflow-x: auto) when it exceeds the width of the parent element, but in the case where a parent element is a table without table-layout: fixed, my injected content instead causes the parent table to expand, horribly breaking some layouts.

In this case, the table is contained within a fixed-width div, something like this:

<div style="width: 600px;">
  <table width="100%">
    <tr>
      <td>
        <div> <!-- my content --> </div>
      </td>
    </tr>
  </table>
</div>

I found that setting table-layout: fixed on the table fixes this problem. However, this markup is beyond my control -- I can only change markup/CSS starting from the innermost div.

I did find one hack that works: setting width: 100%; display: table; table-layout: fixed; on my div. However, I'm not sure if this is compliant with any relevant specs, as the contents of this display: table div are all display: block.

Here is markup that reproduces the problem, as well as demonstrates the hack:

<div class="outer">
  <table class="bad-table">
    <tr>
      <td>
        <div class="wrapper">
          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>
          <div class="target">
            <ul class="menu">
              <li style="background-color: #800;"></li>
              <li style="background-color: #880;"></li>
              <li style="background-color: #080;"></li>
              <li style="background-color: #008;"></li>
            </ul>
          </div>
          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>
        </div>
      </td>
    </tr>
  </table>
</div>

<div class="outer">
  <table class="bad-table">
    <tr>
      <td>
        <div class="wrapper hack">
          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>
          <div class="target">
            <ul class="menu">
              <li style="background-color: #800;"></li>
              <li style="background-color: #880;"></li>
              <li style="background-color: #080;"></li>
              <li style="background-color: #008;"></li>
            </ul>
          </div>
          <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>
        </div>
      </td>
    </tr>
  </table>
</div>

CSS:

.outer {
  width: 200px;
  border: 1px solid #0f0;
}

.bad-table {
  width: 100%;
  border: 1px solid #f00;
}

.target {
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}

.wrapper {
  width: 100%;
}

.wrapper.hack {
  display: table;
  table-layout: fixed;
}

ul.menu {
  list-style: none;
  padding: 0;
  margin: 0;
  white-space: nowrap;
  width: 100%;
}

ul.menu li {
  display: inline-block;
  width: 100px;
  height: 50px;
  padding: 0;
  margin: 0;
}

(Fiddle)

Note that in the first example, the menu blocks cause the table (red border) to expand beyond its containing div (green border). Compare to the second example, which uses my (standards-violating?) hack that successfully prevents the parent table from growing, while also allowing the menu blocks to be scrolled. (Tested with Chrome and Firefox.)

Note the following constraints:

  • I absolutely cannot edit the markup outside of my injected div. This includes manipulating the DOM outside of my injected div, because I don't know what bad effects my changes to someone else's document might cause.
  • The height of my injected div should be based on its contents (as per normal document flow) which means that solutions using position: absolute will tend to be problematic as they will remove my content from the page flow, making preceding and/or following content overlap the injected div. The usual fix for this (setting a fixed height) means my element will be unable to fit its height to match its contents.

Is this hack a legitimate way to solve this problem, or is there a better approach?

cdhowie
  • 158,093
  • 24
  • 286
  • 300
  • Is the outer div's width (in this case 600px, 200px in the demo) known? – Asons Mar 12 '16 at 23:45
  • @LGSon It is not. The goal is for the embedded element to automatically size to fill the width of its direct parent, whatever it may be. – cdhowie Mar 12 '16 at 23:48

3 Answers3

3

table's is always tricky but I have used position: absolute with success in many situations, so my answer/question will be: Will this work for you?

Note though, the wrapper is just a wrapper and should only contain the target, so all content goes into the target, or else the target will overlap its siblings, unless fixed margins/paddings is set.

.outer {
  width: 200px;
}

.bad-table {
  width: 100%;
}

.wrapper {
  position: relative;
}
.target {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: auto;
  overflow: auto;
}

ul.menu {
  list-style: none;
  table-layout: fixed;
  padding: 0;
  margin: 0;
  white-space: nowrap;
  width: 100%;
}
ul.menu li {
  display: inline-block;
  width: 100px;
  height: 50px;
  padding: 0;
  margin: 0;
}
<div class="outer">
  <table class="bad-table">
    <tr>
      <td>
        <div class="wrapper">
          <div class="target">
          
          
            <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>
            <ul class="menu">
              <li style="background-color: #800;"></li>
              <li style="background-color: #880;"></li>
              <li style="background-color: #080;"></li>
              <li style="background-color: #008;"></li>
            </ul>
            <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>


          </div>
        </div>
      </td>
    </tr>
  </table>
</div>

For comparison, here is the same code snippet using display: table

.outer {
  width: 200px;
}

.bad-table {
  width: 100%;
}

.wrapper {
  width: 100%;
  display: table;
  table-layout: fixed;
}
.target {
  overflow: auto;
}

ul.menu {
  list-style: none;
  table-layout: fixed;
  padding: 0;
  margin: 0;
  white-space: nowrap;
  width: 100%;
}
ul.menu li {
  display: inline-block;
  width: 100px;
  height: 50px;
  padding: 0;
  margin: 0;
}
<div class="outer">
  <table class="bad-table">
    <tr>
      <td>
        <div class="wrapper">
          <div class="target">
          
          
            <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>
            <ul class="menu">
              <li style="background-color: #800;"></li>
              <li style="background-color: #880;"></li>
              <li style="background-color: #080;"></li>
              <li style="background-color: #008;"></li>
            </ul>
            <div>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam imperdiet pharetra nunc at condimentum.</div>


          </div>
        </div>
      </td>
    </tr>
  </table>
</div>

Update

After some more testing, and reading these,

I can't find any other way to solve this case, so if there will be content before and after the uncontrolled outer div/table to which your content div's content need to have a normal flow, position: absolute might not work depending how the rest of the page's content is set, but display: table; table-layout: fixed will.

And according to the above sources, there shouldn't be any compliance issues using display: table1, though you need to test the behavior in the major browsers, as they all might deal with div's inside td's different (I did test Chrome,FF,Edge,IE11 on Windows, all worked).


1 Specifically, this text allows for a display: block element directly inside of a display: table element, as the intermediate table-row and table-cell boxes are automatically created as anonymous:

  1. Generate missing child wrappers:
    1. If a child C of a 'table' or 'inline-table' box is not a proper table child, then generate an anonymous 'table-row' box around C and all consecutive siblings of C that are not proper table children.
    2. If a child C of a row group box is not a 'table-row' box, then generate an anonymous 'table-row' box around C and all consecutive siblings of C that are not 'table-row' boxes.
    3. If a child C of a 'table-row' box is not a 'table-cell', then generate an anonymous 'table-cell' box around C and all consecutive siblings of C that are not 'table-cell' boxes.
Community
  • 1
  • 1
Asons
  • 84,923
  • 12
  • 110
  • 165
  • Will this not remove the `.target` element from the page's flow, causing following content to be displayed on top of it? ([Fiddle](https://jsfiddle.net/w865bhtd/4/)) `position: absolute` is going to make it very hard to get preceding and following content to flow correctly around the embedded div. – cdhowie Mar 13 '16 at 00:51
  • In this case the `wrapper` is just a wrapper and should only contain the `target`, so all content goes into the `target`, [updated fiddle](https://jsfiddle.net/LGSon/w865bhtd/5/). Now, depending on how the rest of the page layout is set, this still might not work, so to help with that I need to know that too. – Asons Mar 13 '16 at 09:07
  • Two problems with the new fiddle: (1) *all* of the content is scrollable, while only the menu blocks should be, and (2) since `.target` is removed from the document flow, `.outer` and `.bad-table` now have no height. The outer div will have content before and after it. Consider [this fiddle](https://jsfiddle.net/w865bhtd/10/) -- the exact order of display should be "before outer" - "before wrapper" - menu blocks (scrollable) - "after wrapper" - "after outer". – cdhowie Mar 13 '16 at 19:10
  • I added the relevant text from the CSS2 spec that answers my question perfectly. +1 and accepted. – cdhowie Mar 13 '16 at 19:19
  • 1
    @cdhowie 1:st paragraph, https://www.w3.org/TR/CSS2/tables.html#anonymous-boxes, and yes, that passage, as you don't set the `target` explicit to `display: block`. – Asons Mar 13 '16 at 19:20
  • @cdhowie Then I can't say no more than "Thank you" :) – Asons Mar 13 '16 at 19:22
  • You're welcome, and thanks for the assistance with this issue. I knew there had to be some section of a spec that dealt with the validity of my hack, and I appreciate you locating it for me. (It's not something Google can easily locate unless you already know what you are searching for... trust me, I tried for hours.) – cdhowie Mar 13 '16 at 19:23
  • Side note: I did wind up having to add an intermediate `display: table-cell` element to make Firefox happy when using `height: 100%`. – cdhowie Mar 14 '16 at 19:40
  • @cdhowie Thanks for the note, and I can imagine .. Firefox seems to often need an extra twist when it comes to table's, like with this answer I posted some time ago http://stackoverflow.com/questions/716442/css-cell-margin/21551008#21551008 – Asons Mar 14 '16 at 19:57
0

It's possible to scale an element to its parent's width if that is a table cell, but you will have to workaround some negative consequences. Consider this:

<style>
  table {
    border-collapse: collapse;
    width: 100%;
  }
  td {
    border: 1px solid gray;
    padding: 20px;
  }

  .prevent-table-expand-wrap {
    position: relative;
  }
  .prevent-table-expand-inner {
    /* Add ellipsis. */
    overflow-x: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    /* Make div the width of its parent. */
    position: absolute;
    left: 0;
    right: 0;
    bottom: 0;
  }
</style>

<table>
  <tr><td>
    Column 1.
  </td><td>
    Column 2.
    <div class="prevent-table-expand-wrap">
      &nbsp;
      <div class="prevent-table-expand-inner" title="This contains way too much text, so prevent scaling table">
        Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text. Lots of text.
      </div>
    </div>
  </td><td>
    Column 3.
  </td></tr>
</table>

https://jsfiddle.net/zgwmLms1/19/

The way this works is that the wrapper div just gets width 100% of its container, the table cell. It doesn't have content that affects the scaling of the table columns (well, the nbsp is too narrow). The inner div then gets absolutely positioned and constrained to the wrapper on three sides. Might as well do all 4 sides, but it's not necessary to do both the top and bottom explicitly in this case, as the height of the wrapper and inner are the same.

The biggest difference with the other answer is that mine combines content that does influence the column width, and content that doesn't, in the same table cell. You can see that all 3 columns are evenly sized, that is due to the Column n text.

aross
  • 3,325
  • 3
  • 34
  • 42
-1

Potentially try display: inline-block on the div you want to shrink. It worked for me just now.

Akron
  • 1,413
  • 2
  • 13
  • 28