HowTo: Components – howto-tabs

Summary

<howto-tabs> limit visible content by separating it into multiple panels. Only
one panel is visible at a time, while all corresponding tabs are always
visible. To switch from one panel to another, the corresponding tab has to be
selected.

By either clicking or by using the arrow keys the user can change the
selection of the active tab.

If JavaScript is disabled, all panels are shown interleaved with the
respective tabs. The tabs now function as headings.

All children of <howto-tabs> should be either <howto-tab> or
<howto-tabpanel>. This element is stateless, meaning that no values are
cached and therefore, changes during runtime work.

class HowtoTabs extends HTMLElement {

constructor() {
super();

Event handlers that are not attached to this element need to be bound
if they need access to this.

this._onSlotChange = this._onSlotChange.bind(this);

For progressive enhancement, the markup should alternate between tabs
and panels. Elements that reorder their children tend to not work well
with frameworks. Instead shadow DOM is used to reorder the elements by
using slots.

This element needs to react to new children as it links up tabs and
panel semantically using aria-labelledby and aria-controls.
New children will get slotted automatically and cause slotchange
to fire, so not MutationObserver is needed.

Up until recently, slotchange events did not fire when an element was
upgraded by the parser. For this reason, the element invokes the
handler manually. Once the new behavior lands in all browsers, the code
below can be removed.

The element checks if any of the tabs have been marked as selected.
If not, the first tab is now selected.

const selectedTab =
tabs.find(tab => tab.selected) || tabs[0];

Next, switch to the selected tab. selectTab() takes care of
marking all other tabs as deselected and hiding all other panels.

this._selectTab(selectedTab);
}

_allPanels() returns all the panels in the tab panel. This function
could memorize the result if the DOM queries ever become a performance
issue. The downside of memorization is that dynamically added tabs and
panels will not be handled.

This is a method and not a getter, because a getter implies that it is
cheap to read.

Check if a property has an instance value. If so, copy the value, and
delete the instance property so it doesn't shadow the class property
setter. Finally, pass the value to the class property setter so it can
trigger any side effects.
This is to safe guard against cases where, for instance, a framework
may have added the element to the page and set a value on one of its
properties, but lazy loaded its definition. Without this guard, the
upgraded element would miss that property and the instance property
would prevent the class property setter from ever being called.

Properties and their corresponding attributes should mirror one another.
To this effect, the property setter for selected handles truthy/falsy
values and reflects those to the state of the attribute. It’s important
to note that there are no side effects taking place in the property
setter. For example, the setter does not set aria-selected. Instead,
that work happens in the attributeChangedCallback. As a general rule,
make property setters very dumb, and if setting a property or attribute
should cause a side effect (like setting a corresponding ARIA attribute)
do that work in the attributeChangedCallback(). This will avoid having
to manage complex attribute/property reentrancy scenarios.