This is a follow-up of my Blog about JS / CSS Tabs. It introduces a way how tabs could be displayed in an Accordion manner without changing the structure of HTML. The source code for tabs is re-used, HTML structure is the same, some JS is overridden, some CSS is added.
I called that UI-control Tabcordion. It was inspired by Transformer Tabs on the CSS-Tricks web site. The difference is that Tabcordion (1) is not responsive, more it tries to replace Accordion, and (2) pretends to be ergonomic - the tab-title does not "jump".
Here is an example:
Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim.
At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. Et sea altera salutandi iudicabit. Vidisse probatus moderatius cum te, et vis feugiat luptatum consulatu.
Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu. Eu oblique detraxit honestatis vim, hinc sonet definitiones has ea.
At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. Et sea altera salutandi iudicabit. Vidisse probatus moderatius cum te, et vis feugiat luptatum consulatu.
Lorem ipsum dolor sit amet, qui meliore deserunt at. Percipitur intellegam appellantur cu vim, mei soluta complectitur id, partem reprimique ullamcorper in vim.
Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu. Eu oblique detraxit honestatis vim, hinc sonet definitiones has ea.
The menu-button on the right is visible when all other tab-bars are hidden. When you click the bar, all other tab-bars will show, pushing the currently showing content down. Clicking the bar once more will hide them again, pulling up the content. But when you click one of the other tab-bars, its content will replace the current content. Any tab-bar below the clicked one will disappear then, but none of those above (→ difference to Transformer Tabs).
The disadvantage of Tabcordion is that not all tabs are initially visible. There is a constructor parameter that can change this, but then it looks like tabs that have been arranged vertically.
This might not be a UI-control you want to use, but it shows how well-written JS source could be reused to show completely different results, doing just a few overrides, and adding some CSS.
Click on tab-bar above to navigate to the source code base I'm going to extend now. It is packed in script and style tags, uncommented, ready for copy & paste onto your web page. Mind that style go to head, while script tags should be at end of the body.
For a description of that source go to my last Blog about Tabs.
1 | <script type="text/javascript"> |
[aria-hidden = 'true'] {
display: none;
}
.tabbed-pane > [role = 'tablist'] > [role = 'tab'] {
display: inline-block;
border-radius: 0 1em 0 0;
margin-right: 0.16em;
}
[role = 'tablist'] {
list-style: none;
margin: 0;
padding: 0;
}
[role = 'tab'][aria-selected = 'true'] {
color: white;
background-color: gray;
cursor: initial;
}
[role = 'tab'] {
padding: 0.45em 0.4em 0.2em 0.3em;
color: black;
background-color: #F0F0F0;
cursor: pointer;
margin: 0;
}
[role = 'tab']:focus {
outline: 0;
border: 1px solid red;
}
[role = "tabpanel"] {
padding: 0.2em;
background-color: Khaki;
}
Here is the module outline I am going to implement now, in a script tag. It extends the given instantiated tabBase module (parameter) by setting it to the local that object, and then replacing functions on it. (The tabBase was called tabModule in my last Blog.)
<script type="text/javascript">
/**
* Create a tabcordion.
* @param initiallyExpanded when true, tabcordions will be initially expanded,
* else they will be collapsed and the menu icon will show on selected button.
* @returns the object to call init() upon to find tabbed panes and tabcordions.
*/
var tabcordion = function(tabBase, keyboard, initiallyExpanded)
{
initiallyExpanded = (initiallyExpanded === undefined) ? false : initiallyExpanded;
var that = tabBase;
var initializing = true;
....
var superInit = that.init;
/**
* Builds tabbed panes found in given topElement (or current document).
* Overrides must be done before calling this.
* @param topElement optional, default is document.body,
* the top-element where to search for tabbed-panes.
*/
that.init = function(topElement) {
superInit(topElement);
initializing = false;
};
return that;
};
</script>
The initiallyExpanded parameter defaults to false to make tab-buttons below the focused one initially invisible. Passing true would change that Tabcordion behavior.
The local initializing flag will be used to mark the initialization phase. The init() override sets it to false at end of its execution.
Take this source code to the module outline where the "...." is.
var superSetButtonFocus = that.setButtonFocus;
/**
* Called when a tab was activated by the user.
* This implementation calls super, and then, when isFocused is true,
* toggles CSS "aria-expanded" on the parent container (role="tablist").
* @param tabButton the clicked tab-button.
* @param isFocused true when the tab is going to foreground, false when going to background.
*/
that.setButtonFocus = function(tabButton, isFocused) {
var wasFocused = (tabButton.getAttribute("aria-selected") === "true");
superSetButtonFocus(tabButton, isFocused);
if (isFocused) { // only the focused button can collapse or expand the list parent
var tablist = that.getExpandableParent(tabButton);
if (initializing) // set initial expansion state to the list
tablist.setAttribute("aria-expanded", initiallyExpanded ? "true" : "false");
else if ( ! wasFocused ) // when changing selection, again set initial expansion state to list
tablist.setAttribute("aria-expanded", initiallyExpanded ? "true" : "false");
else // when keeping selection state, toggle expansion state of list
toggleExpandable(tablist);
}
};
var toggleExpandable = function(tablist) {
var expanded = (tablist.getAttribute("aria-expanded") === "true");
tablist.setAttribute("aria-expanded", expanded ? "false" : "true");
};
/**
* This implementation assumes that the "aria-expanded" attribute is carried by direct parent.
* @return the parent element of given button that carries the "aria-expanded" attribute.
*/
that.getExpandableParent = function(tabButton) {
return tabButton.parentElement;
};
var superInstallListeners = that.installListeners;
that.installListeners = function(tabButton, tabPanelsWithButtons, selectedIndex, allTabPanelsWithButtons) {
superInstallListeners(tabButton, tabPanelsWithButtons, selectedIndex, allTabPanelsWithButtons);
if ( ! tabButton.getAttribute("title") )
tabButton.setAttribute("title", "Click to show or hide other tabs");
tabButton.addEventListener(that.observedKeyEvent, function(event) {
var key = keyboard.getKey(event);
if (key === "Enter") {
event.stopPropagation();
event.preventDefault();
toggleExpandable(that.getExpandableParent(tabButton));
}
});
};
Mind that no CSS is set by JS, just WAI-ARIA attributes are used to change the UI state. CSS rules that refer to these attributes will do the real work.
Extending the base module is done by saving function-pointers to local variables and then overwriting these functions. I always call the saved pointers superXXX, for a function XXX. This functional-inheritance-technique is needed when you want to call the super-functions from the override.
Just two functions were overridden:
setButtonFocus() is called on initialization, and whenever the user clicks tab-button. It calls super to do the normal tabbing work, and then, when the tab was focused, toggles the visibility state of the tab-buttons below the current one by setting "aria-expanded".installListeners() was overwritten to also catch the ENTER key on tab-buttons, which should toggle the visibility state of the tab-buttons below. Besides it also puts a tooltip on the tab-button.The getExpandableParent() function refers to the list of tab-buttons. It was made public to be overridden for other determination ways than the direct parent element.
This must go to a style tag in head of your page.
/* tabcordion tab button, with rounded corners and showing always a hand-cursor */
.tabcordion > [role = 'tablist'] > [role = 'tab'] {
border-radius: 0.5em 0.5em 0 0;
cursor: pointer;
margin-bottom: 0.1em;
}
/* show all tabcordion buttons when list is expanded */
.tabcordion > [role = 'tablist'][aria-expanded = 'true'] > [role = 'tab'] {
display: block;
}
/*
Hide buttons below selected when list is collapsed.
This variant does not jump, but items above are always visible.
*/
.tabcordion > [role = 'tablist'][aria-expanded = 'false'] > [role = 'tab'][aria-selected = 'true'] ~ [role = 'tab'] {
display: none;
}
/* tabcordion tablist is parent for absolutely positioned menu-icon */
.tabcordion > [role = 'tablist'] {
position: relative;
}
/* menu-icon as pseudo-element at the very right, visible only when list collapsed */
.tabcordion > [role = 'tablist'][aria-expanded = 'false'] > [role = 'tab'][aria-selected = 'true']::after {
content: '\02630';
margin-right: 0.8em;
position: absolute;
right: 0;
font-family: monospace;
font-size: 120%;
}
/* left indent of a nested tabcordion */
.tabcordion > [role = "tabpanel"] > .tabcordion {
padding-left: 1.3em;
}
Please read the inline comments of these CSS rules. Some of them are just decorative.
Essential is the toggling of tab-button list, done by the two rules
- .tabcordion > [role = 'tablist'][aria-expanded = 'true'] > [role = 'tab']
- .tabcordion > [role = 'tablist'][aria-expanded = 'false'] > [role = 'tab'][aria-selected = 'true'] ~ [role = 'tab']
The selector [role = "tab"] denotes tab-buttons.
The CSS">" operator denotes a direct child, and "~" any following sibling on same level.
CSS rules always point to the rightmost described element.
Thus the second rule says that tab-buttons below the focused one should not be visible when the list of tab-buttons is not aria-expanded.
The .tabcordion > [role = "tabpanel"] > .tabcordion rule indents nested tabcordions. This makes it look a little bit like a tree.
Here is an example Tabcordion.
Following is a Tabcordion containing
You can see it in action on top of this page.
The only structural difference between tabbed pane and Tabcordion is the CSS class on the top-level div element. Thus you could easily turn any Tabcordion into a tabbed pane just by changing the CSS class from "tabcordion" to "tabbed-pane".
<div class="tabcordion">
<ul role="tablist" aria-expanded='true'><li
id="tab-1" role="tab" aria-controls="tab-panel-1" aria-selected="true">Tab One</li><li
id="tab-2" role="tab" aria-controls="tab-panel-2" aria-selected="false">Tab Two</li><li
id="tab-3" role="tab" aria-controls="tab-panel-3" aria-selected="false">Tab Three</li><li>
</ul>
<div role="tabpanel" id="tab-panel-1" aria-labelledby="tab-1">
<p>Lorem ipsum dolor sit amet, qui meliore deserunt at.</p>
</div>
<div role="tabpanel" id="tab-panel-2" aria-labelledby="tab-2">
<div class="tabcordion">
<ul role="tablist"><li
id="tab-2-1" role="tab" aria-controls="tab-panel-2-1" aria-selected="true">Tab Two One</li><li
id="tab-2-2" role="tab" aria-controls="tab-panel-2-2" aria-selected="false">Tab Two Two</li><li
id="tab-2-3" role="tab" aria-controls="tab-panel-2-3" aria-selected="false">Tab Two Three</li>
</ul>
<div role="tabpanel" id="tab-panel-2-1" aria-labelledby="tab-2-1">
<p>At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. </p>
</div>
<div role="tabpanel" id="tab-panel-2-2" aria-labelledby="tab-2-2">
<p>Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu.</p>
</div>
<div role="tabpanel" id="tab-panel-2-3" aria-labelledby="tab-2-3">
<p>At atqui choro theophrastus sit, ne erroribus vulputate vis, eam et antiopam scripserit. </p>
</div>
</div>
</div>
<div role="tabpanel" id="tab-panel-3" aria-labelledby="tab-3">
<div class="tabbed-pane">
<ul role="tablist"><li
id="tab-3-1" role="tab" aria-controls="tab-panel-3-1" aria-selected="true">Tab Three One</li><li
id="tab-3-2" role="tab" aria-controls="tab-panel-3-2" aria-selected="false">Tab Three Two</li><li>
</ul>
<div role="tabpanel" id="tab-panel-3-1" aria-labelledby="tab-3-1">
<p>Lorem ipsum dolor sit amet, qui meliore deserunt at.</p>
</div>
<div role="tabpanel" id="tab-panel-3-2" aria-labelledby="tab-3-2">
<p>Nec an stet decore honestatis, omittam maiestatis ei quo, eripuit facilis recusabo ius cu.</p>
</div>
</div>
</div>
</div>
</div>
Following script should be at end of the body of your page.
<script type="text/javascript">
var domFinder= domUtil();
var keyboardUtil = keyboard();
var tabs = tabBase(domFinder, keyboardUtil);
var initiallyExpanded = false;
tabcordion(tabs, keyboardUtil, initiallyExpanded).init();
</script>
All involved JS modules are instantiated and built together here. Finally init() is called to make the tabcordion module instantiation do its work.
Tabs with WAI-ARIA are a little painful. You always have to provide ids and id-references for aria-controls and aria-labelled-by. Moreover the HTML structure, as proposed by WAI-ARIA, makes it hard to switch between Tabbed-Pane and Accordion without restructuring the HTML, because of the <ul role="tablist"> on top, containing all tab-buttons. For an Accordion, the tab-button would have to be directly above its tab-panel.
In a future Blog I will try to describe how we can organize Tabbed-Panes and Accordions transparently, without having to restructure the HTML. Why don't we want to restructure HTML, for example using JavaScript? Because CSS hardcodes the HTML structure sometimes, and CSS is often written without knowledge about the JS working in the page.
ɔ⃝ Fritz Ritzberger, 2016-04-03