Thursday, October 24, 2013

jQuery Tabs One Tab Bug

I needed to add tabs to my jQuery so, naturally, I selected jQuery Tabs.  It worked great!

Except for a gnarly jQuery bug.  If I had only one tab, the tab background (e.g. the ol/ul element) would be one pixel shorter than if I had two or more tabs.  Also, with only one tab, there was an "off by one" bug where the one and only tab would descend by extra one pixel below the tab background.  This "off by one" effect wasn't that noticeable, especially depending on the various colors involved, which is why it may have slipped by the jQuery Tab developers.

The visual effect would be that, when the user took some action that would add a second tab, the tab background would grow by one pixel and the "off by one" effect would be fixed for all tabs.

I spent a good number of hours hunting for the problem before finding it in jquery.ui.tabs.css.  It contained a rule like this:

.ui-tabs .ui-tabs-nav li.ui-tabs-active {
   margin-bottom: -1px;
   padding-bottom: 1px;

The margin-bottom style with a negative margin was the issue.  The tab background calculated its height according to the height of its child tab (e.g. the li element).  But, apparently, if negative margins are used, the height of the child element is reduced by the negative margin for parent calculation purpose.  For example, if a child element is 15 pixels high and has a margin of -2, the parent sees the child element as 13 pixels high.

The negative margin is only applied to the active tab.  If there is only one tab, the tab background uses only that tab for calculation purposes and is one pixel too short.  If a second tab is added, then, presto!, there is at least one inactive tab.  Inactive tabs don't get the CSS rule that applies the negative margin so the "negative margin discount" isn't applied.  The tab background grows by one pixel so it can contain the inactive tab and solves the issue for all tabs ... at least until there is one tab (which will be the active tab) again.

I confirmed the issue in two ways.  First, I changed the margin-bottom from -1 pixel to -10 pixels and saw the tab background shrink by 9 pixels.  But, of course, when a second tab was added, the height of the tab background jumped by 9 pixels.  Second, I created two tabs and then used the Inspector in my browser to hack in the ui-tabs-active CSS class to both tabs such that there were seemingly two active tabs and zero inactive tabs and was rewarded to see the tab background shrink.

But how to fix?

I tried hacking jQuery Tab JavaScript and modifying the CSS rule above.  Sometimes, I fixed it but then I'd break something else.  Finally, I realized that I needed a way to use the negative margin CSS rule only when there were two or more tabs (which worked already) and then apply a different CSS rule when there was one and only one tab.

CSS3 to the rescue!  As it turns out, there is a way to create a CSS rule that is only applied when an element contains only one child.

.ui-tabs .ui-tabs-nav li:first-child:nth-last-child(1) {
   margin-bottom: 0px;
   position: relative;
   top: 1px;

Rather than changing the CSS rule in jquery.ui.tabs.css, I modify it with my own CSS rule in my own stylesheet that is only applied when there is one child.  My own rule resets margin-bottom from -1 to 0 and then uses position: relative and top: 1px to accomplish the same "border overlap" effect that negative margins were trying to achieve.