Blog-Archiv

Sonntag, 13. März 2016

JS / CSS Tabs

"Tab" is a very vague term. It could mean anything from a trailer to a bottle, a tag, a bill, a stripe ...
Even the computer-world gave it several meanings (press TAB to navigate tabs below by keyboard:-)

There is the TAB key on your keyboard that produces kind of space when used in a text editor, or navigates the user interface by setting the input focus to buttons, choices or text-fields (this is called "tabbing").

This Blog is about how you can get tabs on your web page. It uses tabs itself to give you the feeling of what expects you. You should browse through all tabs in their order when reading this document.

All JS source code shown here is written in pure JavaScript, no jQuery is used.

Term Definitions


TermSemanticWAI-ARIA role
tab-panelthe possibly initially invisible rectangle holding all contents of a tabtabpanel
tab-buttonthe UI-control where you click upon to get a tab-panel to foregroundtab
tabbed panethe top-level container, holding all tab-buttons and tabs-
tab-listthe list of tab-buttons on top of every tabbed panetablist
tabsconvenience term designating a tabbed pane-

Preliminary Considerations


Implementing tabs by pure CSS is not recommendable. See what CSS-Tricks tells about this. The biggest problems may be that

  1. HTML and CSS code have to be very tightly coupled, and
  2. tab-buttons are hyperlinks, so you can not prevent the browser from scrolling down to the link's target, which is not ergonomic (⇾ avoid jumping views).

Pure JavaScript tabs are also not recommendable. Everybody will want to style tabs differently. Be it rounded corners just on one side, or on both, or none at all, custom focus colors, ....

So tabs should be implemented using both CSS and JS.


Implementations

The remainder of this Blog introduces HTML, JS and CSS source-code for tabs.

HTML

Here is an example HTML structure for tabs, as suggested by WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications). All tab-buttons are on top.

<div class="tabbed-pane" style="padding: 1em; border: 1px solid gray;">
    
    <ul role="tablist">
        <li id="tab-1" role="tab" aria-controls="tab-panel-1" aria-selected="true">Keyboard Key</li><!--
     --><li id="tab-2" role="tab" aria-controls="tab-panel-2" aria-selected="false">Text Space</li><!--
     --><li id="tab-3" role="tab" aria-controls="tab-panel-3" aria-selected="false">User Interface Element</li>
    </ul>
      
    <p role="tabpanel" id="tab-panel-1" aria-labelledby="tab-1">
        There is the TAB key on your keyboard ....
    </p>

    <p role="tabpanel" id="tab-panel-2" aria-labelledby="tab-2">
        There may be tab-spaces in the text ....
    </p>

    <p role="tabpanel" id="tab-panel-3" aria-labelledby="tab-3">
        The user interface could use tabs ....
    </p>
</div>

JS

Tab-Module Outline

Put this script tag on bottom of your HTML page body. Copy all subsequent functions (except the utility modules) to where the "...." is.

<script type="text/javascript">

    var tabModule = function(domUtil, keyboard)
    {
      var that = {};
      
      that.observedKeyEvent = "keydown";
      
      ....

      that.init = function(topElement) {
        initTabbedPanes(topElement);
      };
      
      return that;     
    };

    tabModule(domUtil(), keyboard()).init();

</script>

The trailing line tabModule(domUtil(), keyboard()).init(); installs the tab functionality.

Tab-Module Functions

All of the following go to the Tab-Module script to where the "...." is.

The finder functions outline how the module works:

  • first it finds all tabbed panes, for each tabbed pane
    • it finds all tab-panels, for each tab-panel
      • it finds the tab-button
      • every button together with its panel is packed into a JS object
    • all of these objects are collected into an array
    • that array describes exactly one tabbed pane, and it is the return of function initTabs(), which is among the initializers.
      /**
       * Assumes that the direct parent of an element
       * with role "tablist" is a tabbed pane.
       * @return a list of all tabbed panes below given topElement.
       */
      that.findTabbedPanes = function(topElement) {
        var tabLists = topElement.querySelectorAll("[role = 'tablist']");
        var result = [];
        for (var i = 0; i < tabLists.length; i++)
          result.push(tabLists[i].parentElement);
        return result;
      };
      
      /**
       * Uses domUtil to find all tab-panels within a tabbed-pane,
       * not including tab-panels of nested tabbed panes.
       * @return a list of tab-panels nested directly into given tabbed pane.
       */
      that.findTabPanels = function(tabbedPane) {
        return domUtil.findChildren(
            tabbedPane,
            function(childElement) {
              return childElement.getAttribute("role") === "tabpanel";
            }
        );
      };
      
      /**
       * @return the tab-button for given tab-panel below given tabbed pane,
       *    considering all WAI-ARIA variants of describing it.
       */
      that.findTabButton = function(tabPanel, tabbedPane) {
        var tabButtonId = tabPanel.getAttribute("aria-labelledby");
        var result = document.getElementById(tabButtonId);
        
        if ( ! result )
          result = tabbedPane.querySelector("[aria-controls = '"+tabPanel.id+"']");
        
        if ( ! result )
          throw "Could not find button for tab id="+tabPanel.id;
        
        return result;
      };
      
      /*
       * @return true when given tab is currently selected,
       *     i.e. its "aria-selected" attribute is "true".
       */
      that.isSelectedTab = function(tabButton, tabPanel, tabbedPane) {
        return tabButton.getAttribute("aria-selected") === "true";
      };

Utility Modules

Copy these into a <script type="text/javascript"> tag before the Tab-Module script.

Following DOM module enables me to find child elements, recursively, that comply with a criterion given by a JS function. When a child matches, its children are not iterated. That way I do not find a tab-panel nested in a tab-panel when looking for tab-panels.

    var domUtil = function()
    {
      var that = {};
    
      /**
       * Returns children found by given matchFunction, recursively,
       * but not going into elements where matchFunction is successful.
       * @param topElement required, the element where to start recursive search.
       * @param matchFunction required, elements for which this function returns true will be returned.
       * @return the array of found elements (not nodes).
       */
      that.findChildren = function(topElement, matchFunction, found) {
        found = found || [];

        for (var i = 0; i < topElement.children.length; i++) {
          var child = topElement.children[i];

          if (matchFunction(child))
            found.push(child);
          else
            that.findChildren(child, matchFunction, found);
        }
        return found;
      };

      return that;
    };

CSS

Put following CSS styles into a <style type="text/css"> tag in head of your HTML.

The tabbed pane styles are essential, you should not leave them out. They arrange the list of buttons to be horizontal. But of course you can customize the corner rounding.

      /* do not display hidden tab panels */
      [aria-hidden = 'true'] {
        display: none;
      }
      
      /* a tab-button below a tabbed pane (not below an accordion) */
      .tabbed-pane > [role = 'tablist'] > [role = 'tab'] {
        display: inline-block; /* horizontally arranged */
        border-radius: 0 1em 0 0; /* rounded top-right corner */
        margin-right: 0.16em; /* a little space to next button */
      }
      
      /* tab button bar */
      [role = 'tablist'] {
        list-style: none; /* removes bullets */
        margin: 0; /* removes indentation */
        padding: 0;
      }

Quite a lot of source code to achieve tabs!
Visit the newest state of this project on my homepage.




Keine Kommentare: