Blog-Archiv

Montag, 25. Januar 2016

JS clientWidth and CSS width

It is not easy to get clearness about the relation between JavaScript and CSS properties. Best is to write a test page and try it out. Exactly this I did here to study the JS properties clientWidth and offsetWidth, and some other properties around element dimensions, and how CSS width, padding, border and margin influence them. (Please mind that I do not cover the CSS box-sizing property here!)

  • clientWidth and clientHeight include padding, but exclude border, whereby both will be zero and ignored on inline elements
  • offsetWidth and offsetHeight include both padding and border, and is always identical with getBoundingClientRect()
  • neither includes margin
  • clientTop and clientLeft give the border thickness
  • offsetTop and offsetLeft give the pixel distances from page edges

All these JS properties in HTML element objects are read-only. When you want to set a width, you need to do it via CSS:

    var element = document.getElementById("myElement");
    alert("clientWidth = "+element.clientWidth);

    element.style.width = 20+"px";
    alert("clientWidth = "+element.clientWidth);

Here is something to play around. The small yellow-green test-DIV to be seen below is following HTML:

    <div style="border: 1px solid gray;">
      <div id="block-element" style="width: 60px; background-color: LightGreen; border: 0px solid green;">
        <span style="background-color: yellow;">DIV</span>
      </div>
    </div>

The outer DIV provides a gray border that makes margins visible. The input fields set their values upon the inner green DIV. It contains a yellow SPAN to make the inner paddings better visible.

Test View
width
height
margin
border-width
padding
clientWidth
offsetWidth
boundingClientRect.width
clientHeight
offsetHeight
boundingClientRect.height
 
clientTop
clientLeft
 
offsetTop
offsetLeft

DIV

You can input pixel sizes in the fields to the left. That will change the according CSS properties of the green DIV below. The display to the right shows how these CSS-properties then influence the JS DOM element properties.

Here is some JS source to calculate the CSS width from JS clientWidth.

      var style = window.getComputedStyle(element);
      var paddings = window.parseInt(style["padding-left"]) + window.parseInt(style["padding-right"]);
      return element.clientWidth - paddings;

Should you need to know the left and right margins of some element, do this:

      var style = window.getComputedStyle(element);
      var margins = window.parseInt(style["margin-left"]) + window.parseInt(style["margin-right"]);

Need to find out left + right border width?

      var borders = element.offsetWidth - element.clientWidth;

When you want to set two elements to same visible width, do this:

    var getPaddingsLeftRight = function(element) {
      var style = window.getComputedStyle(element);
      return window.parseInt(style["padding-left"]) + window.parseInt(style["padding-right"]);
    };

    var setSameWidth = function(sourceElement, targetElement) {
      var targetPaddings = getPaddingsLeftRight(targetElement);
      targetElement.style.width = (sourceElement.clientWidth - targetPaddings)+"px";
    };

Please mind that this JS code would not work for elements that have the box-sizing property set to border-box, or inherited that value.




Dienstag, 19. Januar 2016

Space in HTML Breaks Layout

Some things are hard to understand. Actually they can't be understood, they can just be learnt and accepted. Like von Neumann said: "In mathematics you don't understand things. You just get used to them".

CSS display: inline-block

Have a look at this HTML and CSS:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  
  <style type="text/css">
    .inline-block {
      display: inline-block;
    }
  </style>

</head>
  
<body>

  <div style="background-color: SkyBlue; height: 10em; padding: 1em;">

    <div class="inline-block" style="background-color: yellow;">
      Box 1
    </div>

    <div class="inline-block" style="background-color: orange;">
      Box 2
    </div>

  </div>
  
</body>
</html>

Here is what this looks like:

Box 1
Box 2

This is how display: inline-block is documented to work: it arranges elements horizontally.

But not always. Lets add a little more text to the boxes.

  <div style="background-color: SkyBlue; height: 10em; padding: 1em;">

    <div class="inline-block" style="background-color: yellow;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
    </div>

    <div class="inline-block" style="background-color: orange;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
    </div>

  </div>

This looks like the following now:

Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.

Erm - not actually what was expected, right?

Add Width

We learn that we need to give these boxes a width. And as it is hard to define an exact width for text contents, we define a percentage width.

  <style type="text/css">
    .inline-block {
      display: inline-block;
      width: 50%;
    }
  </style>

Now it looks like this:

Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.

Not much changed - Hrm?
This is the point where you look for stackoverflow. And you learn the lesson you have to get used to:

white-space between the two <div> elements is "significant"

That means it is physically rendered, and is the reason why 2 * 50% do not fit into the container. You can't see it, but it is there.

Remove Spaces

What we probably hardly will get used to is that there is no CSS fix for this. You need to either do some very noisy workarounds affecting the parent element and its font-size, or you need to simply remove the spaces between the <div> elements in HTML!

  <div style="background-color: SkyBlue; height: 10em; padding: 1em;">

    <div class="inline-block" style="background-color: yellow;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
    </div><div class="inline-block" style="background-color: orange;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
    </div>

  </div>

Ugly HTML, but works:

Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.

That's what we expected.

Until the new developer goes over that unreadable code and formats it :-(moment to think about sustainability)-:

Yes, we need to document that this is a necessary workaround for the inline-block space problem!
And as we are commenting now, maybe there is a better way to remove the space, couldn't we comment it out?

  <div style="background-color: SkyBlue; height: 10em; padding: 1em;">

    <div class="inline-block" style="background-color: yellow;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
    </div><!--
          Do not remove this workaround for the inline-block space problem.
 --><div class="inline-block" style="background-color: orange;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
    </div>

  </div>

This looks much better, and is documented seriously.

Use box-sizing: border-box, avoid margin

This journey could be continued with border, padding and margin, all of these will also break the layout. For borders and paddings you could help yourself with box-sizing: border-box, but for margins everything is in vain, best you avoid it.

Vertical Align

But let's also learn the last inline-block lesson. We make the text in the right box a little shorter, and see what happens then.

  <div style="background-color: SkyBlue; height: 10em; padding: 1em;">

    <div class="inline-block" style="background-color: yellow;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
    </div><!--
          Do not remove this workaround for the inline-block space problem.
    --><div class="inline-block" style="background-color: orange;">
      Lorem ipsum dolor sit amet, qui meliore deserunt at.
    </div>

  </div>

Here is what this looks like:

Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
Lorem ipsum dolor sit amet, qui meliore deserunt at.

Erm - no, not what was expected.
But for this we have some CSS. It needs vertical-align: top.
Here is the final CSS for display: inline-block elements.

  <style type="text/css">
    .inline-block {
      display: inline-block;
      width: 50%;
      vertical-align: top;
    }
  </style>

This looks good now:

Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim. Eam an porro accusamus dissentiunt, te sit impedit tacimates, movet laboramus mea ad.
Lorem ipsum dolor sit amet, qui meliore deserunt at.

Hm, that space will stay significant for quite a time ...




Montag, 18. Januar 2016

JS Titled Border

Titled Border

Not that I want to repeat all those desktop user interface patterns in the browser environment, but a titled border is simply a beautiful and elegant way to style a chapter in a web page.


This is a HTML fieldset with legend

HTML provides this by the FIELDSET element. When you nest a LEGEND element into it, you will also get a title and a border. I wrote this article before I detected that element type.

WARNING: this article uses pure JavaScript (JS), no jQuery:-(, and ignores web-browsers that do not follow standards.

Here comes a CSS-only variant of "titled border", without JS.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="initial-scale=1"/>
    
    <style>
      .titled-border {
        border: 1px solid gray;
        border-radius: 0.5em;
        position: relative; /* be parent for absolutely positioned title */
        padding-top: 0.5em; /* leave half of the title height space on top */
      }
      .border-title {
        position: absolute; /* go out of the layout, let followers fill up */
        top: -0.5em; /* slip upwards */
        left: 0.4em; /* distance to border corner */
        background-color: white; /* cover underlying border */
        padding: 0 0.3em 0 0.3em; /* leave space between border and text, left and right */
      }
    </style>
 
  </head>
  
  <body>
  
    <div class="titled-border">
      <span class="border-title"><b>Titled Border</b></span>
      
      <div>
        Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim.
      </div>
    </div>
      
  </body>
</html>

I created two CSS rules-sets, the selector .titled-border is for the border and .border-title is for the text.

The .titled-border carries the border specification, and a relative positioning, which is for being the coordinate system to the absolutely positioned title element. Further it specifies a padding-top to make place for the title that will be lifted there.

The .border-title is positioned absolutely, it goes upwards half a letter height, and a little to the left to keep distance from the border corner. Then it needs a defined background-color to not let strike through the border. The final paddings are the gaps between the corder and the title.

The HTML is simple, any border is an enclosing div, and the title is its first child element. Best are inline-elements like span, as they do not expand across the whole width, like block-elements do.

Is it that easy?

I thought I was done, and tried this out in combination with different HTML. Here is a screenshot of the result.

As you see, there is a significant problem with positioning the title text, and letting the border be visible where it should be. Easy only for left-aligned titles, and the real problems start when using a <H1> as title, because this has margins and is full-width.

Write CSS for each such box?

So I faced the necessity to either write new CSS for every bordered title I come across, or develop this further using JavaScript (JS). Of course I wanted to reuse a concept that makes it easy to style the title and the border independently of their CSS declarations.

Here comes my titled-border solution

Here comes my titled-border solution!

CSS

    <style type="text/css">
      .titled-border {
        border: 2px solid gray;
        border-radius: 0.5em;
      }
    </style>

Not much CSS is left. Positioning must be done dynamically, same for the background-color of the text. You could even leave out this CSS and style each border individually via inline-styles. But mind that the (now following) JS will search for the classes titled-border and border-title, so you need at least to set titled-border upon the border-element. By default the first child could count as title, so this class can be ommitted, except on nested titled borders, where the JS would fail when border-title was not set upon the title element.

JS

Here is the scetch of the script that will evolve now.
You can find the complete source on bottom of this page, or on my homepage.

    <script type="text/javascript">
      "use strict";

      var titledBorderFactory = function(TITLED_BLOCK_CLASS, BLOCK_TITLE_CLASS)
      {
        var that = {};

        // ....

        that.init = function(topElement) {
          topElement = topElement || document.body;
          // ....
        };

        return that;
      };

      titledBorderFactory("titled-border", "border-title").init();
    
    </script>

This is the closure (or module) that encapsulates functions and variables working on titled borders. As top-level factory-function parameters I want to pass the CSS class-names of the titled-border and the border-title, so that this is not restricted to titled-border and border-title. Then I would like to pass an optional top-level element where to search below for titled borders, in case I want to have different areas on my page. The default for this would, of course, be the document.body element to process the whole page. All the now following functions go into that closure.

Find Titled Border Elements

Here is the init() implementation, which could be understood as constructor.

        /**
         * Initializes all titled blocks below given element or in whole document.
         * @param topElement optional, the container to search for titled borders, default is document.body.
         */
        that.init = function(topElement) {
          topElement = topElement || document.body;
          var titledBlocks = topElement.querySelectorAll("."+TITLED_BLOCK_CLASS);

          for (var i = titledBlocks.length - 1; i >= 0; i--) {
            var titledBlock = titledBlocks[i];
            var title = titledBlock.querySelector("."+BLOCK_TITLE_CLASS); /* find first child with given CSS class */
            if ( ! title ) /* by default take first child */
              title = titledBlock.children[0];

            if (title)
              positionTitle(titledBlock, title);
          }
        };

This sets the default in case no parameter has been given. Then it searches for CSS class titled-border below the top element. For each of them, it retrieves the related title element and passes both for further processing to positionTitle().

Position the Title on the Border

Here comes the hard layout work to be done. It is commented extensively, so try to read the comments and understand the source by them. That is what comments are made for. Functions used here may be implemented in the utils on bottom.

        /** Moves the title into a div that is created as first child of the block. */
        var positionTitle = function(titledBlock, title) {
          var titleHeight = title.offsetHeight; /* height including padding and border */
          var titleStyle = window.getComputedStyle(title); /* need to calculate margins */
          var titleMargins = getTopAndBottomMargins(titleStyle);
          var fullTitleHeight = titleHeight + titleMargins;

          var titleContainer = createTitleContainer(titleHeight, fullTitleHeight, getAlignment(titleStyle));
          styleTitle(titleStyle, title, titledBlock);
          styleTitledBlock(titleHeight, titledBlock);

          titledBlock.insertBefore(titleContainer, titledBlock.children[0]); /* insert titleContainer at start */
          title.parentElement.removeChild(title); /* move title from parent to titleContainer */
          titleContainer.appendChild(title);
        };

This finds together some variables that are used to style the elements now. Most important is the full height of the title. The negative half of this will be the top offset. Mind that the computed style is used here, because with element styles you might see only the inline-element margins.

Then a new element is created, to be used as wrapper for the title. This is needed to align the title text left, center or right, given by its own text-align CSS property. The new element is styled in its own function, as are the border and the title.

Finally the new element is inserted as first child into the border-container, and the title element is moved into the new element.

        /** Creates title container that allows alignment. */
        var createTitleContainer = function(titleHeight, fullTitleHeight, alignment) {
          var titleContainer = document.createElement("div"); /* need a new element */
          
          titleContainer.style.position = "absolute"; /* go off the layout, let fill up following elements */
          titleContainer.style["background-color"] = "transparent"; /* let shine through border */
          titleContainer.style.top = (-fullTitleHeight / 2)+"px"; /* slip upwards above the border */
          titleContainer.style.left = "0"; /* to enable alignment, stick to the left */
          titleContainer.style.width = "100%"; /* to enable alignment, take whole width */
          titleContainer.style["box-sizing"] = "border-box"; /* do not cause overflows */
          titleContainer.style["text-align"] = alignment; /* adopt alignment from title */
          if (alignment !== "center") /* keep distance from border-corner */
            titleContainer.style["padding-"+alignment] = (titleHeight / 3)+"px";
          
          return titleContainer;
        };

        /** Prepares title. */
        var styleTitle = function(titleStyle, title, titledBlock) {
          var backgroundColor = titleStyle["background-color"];
          if (isTransparent(backgroundColor )) /* must not be transparent, border would strike through */
            title.style["background-color"] = findParentBackgroundColor(titledBlock);
          
          title.style["padding-left"] = "0.2em"; /* distance between text and border */
          title.style["padding-right"] = "0.3em";
          title.style["display"] = "inline-block"; /* makes block-elements lose 100% width, this enables alignment */
          title.style["white-space"] = "nowrap"; /* do not break title into new line */
          title.style["overflow"] = "hidden"; /* do not write into other elements */
        };

        /** Prepares bordering block. */
        var styleTitledBlock = function(titleHeight, titledBlock) {
          titledBlock.style.position = "relative"; /* be parent for absolute titleContainer */
          titledBlock.style["margin-top"] = (titleHeight / 2)+"px"; /* avoid title to slip out of page on top */
          titledBlock.style["padding-top"] = (titleHeight / 2)+"px"; /* avoid followers to overlap with title above */
        };

The title-container needs to be absolutely positioned towards the relative border-container. It goes upwards (negative top) half of its height. It must be transparent to let the border strike through, and it needs to be full-width to enable alignment. Then it sets the alignment, avoiding layout problems by using box-sizing: border-box.

The title styles must guarantee that the border does not strike through, using a background color. This is retrieved from the parents when not declared on the element itself, default is white. Important here is the display: inline-block style. It makes any title that was defined as block element shrink its width to what is really needed. Only that way I can align in its container block.

The border element finally needs to be positioned relatively, because it will be the coordinate system for the absolute title-container. It must provide a margin-top for the title to not slip out of the page on top, and a padding-top for the following child elements to not slip into the lower part of the title.

Utils

Here are the remaining functions that have been used in the code above.

        var findParentBackgroundColor = function(element) {
          while (element && element !== document.documentElement) {
            var style = window.getComputedStyle(element);
            var backgroundColor = style["background-color"];
            if (backgroundColor && ! isTransparent(backgroundColor))
              return backgroundColor;

            element = element.parentElement;
          }
          return "white";
        };

        var isTransparent = function(color) {
          return color === "transparent" || color === "rgba(0, 0, 0, 0)";
        };

        var getAlignment = function(style) {
          var alignment = style["text-align"];
          return ! alignment ? "left" : (alignment === "start") ? "left" : alignment;
        };

        var getTopAndBottomMargins = function(style) {
          return window.parseInt(style["margin-top"]) + window.parseInt(style["margin-bottom"]);
        };

Mind that always the computed style is passed to these functions, not the element-style. Remarkable may be the isTransparent() function, amazingly there is no standard for this. Also remarkable the text-align property value "start" that I encountered on Webkit browsers.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
      "use strict";

      /**
       * Creates an object with an init() function that can layout titled borders.
       * @param TITLED_BLOCK_CLASS optional, name of CSS class designating the bordered container, without leading dot.
       * @param BLOCK_TITLE_CLASS optional, name of CSS class designating the title, without leading dot.
       * @returns an object to call init().
       */
      var titledBorderFactory = function(TITLED_BLOCK_CLASS, BLOCK_TITLE_CLASS)
      {
        var that = {};

        var findParentBackgroundColor = function(element) {
          while (element && element !== document.documentElement) {
            var style = window.getComputedStyle(element);
            var backgroundColor = style["background-color"];
            if (backgroundColor && ! isTransparent(backgroundColor))
              return backgroundColor;

            element = element.parentElement;
          }
          return "white";
        };

        var isTransparent = function(color) {
          return color === "transparent" || color === "rgba(0, 0, 0, 0)";
        };

        var getAlignment = function(style) {
          var alignment = style["text-align"];
          return ! alignment ? "left" : (alignment === "start") ? "left" : alignment;
        };

        var getTopAndBottomMargins = function(style) {
          return window.parseInt(style["margin-top"]) + window.parseInt(style["margin-bottom"]);
        };

        /** Creates title container that allows alignment. */
        var createTitleContainer = function(titleHeight, fullTitleHeight, alignment) {
          var titleContainer = document.createElement("div"); /* need a new element */
          
          titleContainer.style.position = "absolute"; /* go off the layout, let fill up following elements */
          titleContainer.style["background-color"] = "transparent"; /* let shine through border */
          titleContainer.style.top = (-fullTitleHeight / 2)+"px"; /* slip upwards above the border */
          titleContainer.style.left = "0"; /* to enable alignment, stick to the left */
          titleContainer.style.width = "100%"; /* to enable alignment, take whole width */
          titleContainer.style["box-sizing"] = "border-box"; /* do not cause overflows */
          titleContainer.style["text-align"] = alignment; /* adopt alignment from title */
          if (alignment !== "center") /* keep distance from border-corner */
            titleContainer.style["padding-"+alignment] = (titleHeight / 3)+"px";
          
          return titleContainer;
        };

        /** Prepares title. */
        var styleTitle = function(titleStyle, title, titledBlock) {
          var backgroundColor = titleStyle["background-color"];
          if (isTransparent(backgroundColor )) /* must not be transparent, border would strike through */
            title.style["background-color"] = findParentBackgroundColor(titledBlock);
          
          title.style["padding-left"] = "0.2em"; /* distance between text and border */
          title.style["padding-right"] = "0.3em";
          title.style["display"] = "inline-block"; /* makes block-elements lose 100% width, this enables alignment */
          title.style["white-space"] = "nowrap"; /* do not break title into new line */
          title.style["overflow"] = "hidden"; /* do not write into other elements */
        };

        /** Prepares bordering block. */
        var styleTitledBlock = function(titleHeight, titledBlock) {
          titledBlock.style.position = "relative"; /* be parent for absolute titleContainer */
          titledBlock.style["margin-top"] = (titleHeight / 2)+"px"; /* avoid title to slip out of page on top */
          titledBlock.style["padding-top"] = (titleHeight / 2)+"px"; /* avoid followers to overlap with title above */
        };

        /** Moves the title into a div that is created as first child of the block. */
        var positionTitle = function(titledBlock, title) {
          var titleHeight = title.offsetHeight; /* height including padding and border */
          var titleStyle = window.getComputedStyle(title); /* need to calculate margins */
          var titleMargins = getTopAndBottomMargins(titleStyle);
          var fullTitleHeight = titleHeight + titleMargins;

          var titleContainer = createTitleContainer(titleHeight, fullTitleHeight, getAlignment(titleStyle));
          styleTitle(titleStyle, title, titledBlock);
          styleTitledBlock(titleHeight, titledBlock);

          titledBlock.insertBefore(titleContainer, titledBlock.children[0]); /* insert titleContainer at start */
          title.parentElement.removeChild(title); /* move title from parent to titleContainer */
          titleContainer.appendChild(title);
        };

        /**
         * Initializes all titled blocks below given element or in whole document.
         * @param topElement optional, the container to search for titled borders, default is document.body.
         */
        that.init = function(topElement) {
          topElement = topElement || document.body;
          var titledBlocks = topElement.querySelectorAll("."+TITLED_BLOCK_CLASS);

          for (var i = titledBlocks.length - 1; i >= 0; i--) {
            var titledBlock = titledBlocks[i];
            var title = titledBlock.querySelector("."+BLOCK_TITLE_CLASS); /* find first child with given CSS class */
            if ( ! title ) /* by default take first child */
              title = titledBlock.children[0];

            if (title)
              positionTitle(titledBlock, title);
          }
        };

        return that;
      };

      /* initialize all titled borders */
      titledBorderFactory("titled-border", "border-title").init();
    



Samstag, 16. Januar 2016

HTML Input Focus Tab Order

Wherever you mouse-click on a web-page, or finger-tap, the input-focus will be set to that location. In case a focusable field is at that location, you actually can interact with the page then. But keyboard-navigating to such a field on a desktop computer mostly is an uncertain adventure.

By default, only focusable elements are reachable by keyboard. These are just

  • <a>
  • <input>
  • <select>
  • <textarea>
  • <button>

But you can make any element focusable by setting a tabindex attribute to it.

  • tabindex = "-1" will make the element focusable by mouse, or by a JS element.focus() call, but it won't be reachable by keyboard

  • tabindex = "0", the default for any focusable element without tabindex, will make the element reachable also by keyboard, but the tabbing order will be that of the HTML elements

  • tabindex = "1" or bigger will make the element focusable, and the tabbing order will be that of the tabindex values, from smallest to biggest; further these elements will be focused before those with tabindex = "0"

It is a little difficult to demonstrate tabindex on this Blog page, because here are a lot of links that request focus when tabbing through. Thus you should watch this here on my homepage.

But if your browser supports iframes, you may see the test view below. The iframe serves as local tabbing range. The tabindex values are rendered in red text to the right of the field. Focused fields will have a red border. First click this

button. When you press "Tab" after, the focus will move to the iframe. Then continue with the "Tab" key, and watch which elements are focused, and in which order. Finally the focus will go out of the iframe and continue with some Blog links. Mind that the button "Five" never will receive the focus, although it is a focusable element, but it has an explicit tabindex = "-1".

The tabbing rules are:

  1. start with the element having the lowest tabindex, starting from 1
  2. continue with next higher tabindex, ignoring gaps
  3. when having same tabindex, the HTML-order decides which is next
  4. when no more positive tabindex numbers are left, go to first in element-order with tabindex="0", whereby focusable elements without tabindex automatically have tabindex="0"
  5. do all tabindex="0" elements in their HTML-order

The focus will never go to an element having tabindex="-1" while using the keyboard. Nevertheless, when you mouse-click on it, it will have the focus. In case it does not have a tabindex at all, it can be focused only when it is a focusable element (see above).

As identical tabindex values will not be ignored, you can build tabbing groups by using the same tabindex for several related elements. But mind that then the HTML order counts. For example, you could have 3 elements using tabindex = "1", then 5 using tabindex = "2", and so on.

My browser, after having finished this traversal, goes to the "page-identity" toolbar button, then to the URL address line, then to the search-field, then back to the client-area, then starts again with tabindex elements.


Here is the source code of the test page. Copy & paste it into a .html file and try it out using a file:/// URL.

The tabindex attribute has been made visible by an ::after pseudo-element. There you can fetch attribute values in the content property by using the CSS function attr().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1"/>
    <title>Trace Input Focus</title>
    
    <style type="text/css">
      *:focus {
        border: 2px solid red;
      }
      *::after {
        content: attr(tabindex);
        padding-left: 1em;
        color: Tomato;
      }
    </style>
    
  </head>
  
  <body>
      
    <div>
      <div tabindex="0">One</div>
      <div tabindex="2">Two</div>
      <div tabindex="1">Three</div>
    </div>

    <div>
      <button>Four</button> <br>
      <button tabindex="-1">Five</button> <br>
      <button tabindex="3">Six</button> <br>
      <button tabindex="0">Seven</button>
    </div>

    <div>
      <p tabindex="8">Eight</p>
      <p tabindex="9">Nine</p>
      <p tabindex="8">Ten</p>
    </div>
  
  </body>

</html>

You can find further background information here.