🛠️ Tabby: Refactor
We can make many improvements to Tabby’s code. Let’s begin by refactoring the HTML.
Refactoring Tabs
Take a look at the HTML for each tab. You’ll notice we used the tab
class and role="tab"
for each tab.
<button
role="tab"
class="tab is-selected"
aria-selected="true"
data-target="digimon"
data-theme="digimon"
> Digimon </button>
The tab
class and role="tab"
contains duplicated information. They both mean the tab is a tab.
If we use role="tab"
in the CSS and JavaScript, we don’t need the tab
class anymore.
Here’s the simplified HTML:
<!-- Digimon button -->
<button
role="tab"
class="is-selected"
aria-selected="true"
data-target="digimon"
data-theme="digimon"
> Digimon </button>
Here are the required CSS changes.
/* Change this */
.tab { /* ... */ }
/* To this */
[role="tab"] { /* ... */ }
If we want to be more specific, we can add .tabby
to the selector.
/* Change this */
[role="tab"] { /* ... */ }
/* To this */
.tabby [role="tab"] { /* ... */ }
We also need to change to use role="tab"
instead of .tab
. Here are the required changes:
// Change this
const tabs = [...tabby.querySelectorAll('.tab')]
// To this
const tabs = [...tabby.querySelectorAll('[role="tab"]')]
Refactoring Tablist
Since we used role="tab"
for each tab, it make sense for us to use role="tablist"
instead of .tabs
.
<!-- Change this -->
<div class="tabs" role="tablist" aria-label="Virtual Pets">
<!-- To this -->
<div role="tablist" aria-label="Virtual Pets">
Here are the required CSS changes:
/* Change this */
.tabs { /* ... */ }
/* To this */
.tabby [role="tablist"] { /* ... */ }
And the JavaScript changes:
// Change this
const tabsList = tabby.querySelector('.tabs')
// To this
const tabsList = tabby.querySelector('[role="tablist"]')
Refactoring Tabpanel
In the same vein, we can use the role="tabpanel"
instead of .tab-content
. If we do this, we can remove the .tab-content
from the HTML.
<!-- Digimon tabpanel -->
<section
role="tabpanel"
class="is-selected"
id="digimon"
data-theme="digimon"
aria-labelledby="digimon-title"
tabindex="-1"
> ... </section>
Here are the required CSS changes:
/* Change this */
.tab-content { /* ... */}
/* To this */
.tabby [role="tabpanel"] { /* ... */}
// Change this
const tabContents = [...tabby.querySelectorAll('.tab-content')]
// To this
const tabContents = [...tabby.querySelectorAll('[role="tabpanel"]')]
Tabpanel vs Tab-content
We have two terms that mean the same thing now: “Tabpanel” and “Tab-content”. We want to use one term to make things consistent.
Let’s use “Tabpanel” since it’s the official language.
We need to make these HTML changes:
<!-- Change this -->
<div class="tab-contents">
<!-- To this -->
<div class="tabpanels">
The CSS changes:
/* Change this */
.tab-contents { /* ... */}
/* To this */
.tabpanels { /* ... */}
And JavaScript changes:
// Change this
tabContent
// To this
tabPanel
Selecting a tab
We used two methods to denote the selected tab:
- The
is-selected
class
aria-selected="true"
We can choose one of them. Let’s use the aria-selected
method.
First, we remove is-selected
from the HTML:
<!-- Digimon button -->
<button
role="tab"
aria-selected="true"
data-target="digimon"
data-theme="digimon"
> Digimon </button>
Then, we change .is-selected
to aria-selected="true"
in the CSS:
/* Change this */
tabby [role="tab"].is-selected { /* ... */ }
/* To this */
tabby [role="tab"][aria-selected="true"] { /* ... */ }
And we remove code that contains .is-selected
from the JavaScript.
const selectTab = tab => {
// ...
tabs.forEach(t => {
// Remove this line
t.classList.remove('is-selected')
// ...
})
// Remove this line
tab.classList.add('is-selected')
// ...
}
document.addEventListener('keydown', event => {
// ...
// Change this
const index = tabs.findIndex(t => t.classList.contains('is-selected'))
// To this
const index = tabs.findIndex(t => t.getAttribute('aria-selected') === 'true')
// ...
})
Selecting a tabpanel
Right now, we identify the selected Tab with aria-selected="true"
. But we still identify the selected Tabpanel .is-selected
.
There’s a conflict between these two naming conventions.
It would be great if we could use aria-seleceted="true"
to identify the selected Tabpanel. But we can’t. It’s wrong to use aria-selected
this way.
We can resolve the naming conflict in two ways:
- Use
data-selected="true"
instead of aria-selected="true"
. The styles are similar.
- Hide unselected tabs with the
hidden
attribute.
Both ways work. We’ll use the second method.
First, we need remove the is-selected
styles from the CSS:
/* Remove these */
.tabby [role="tabpanel"] {
display: none;
}
.tabby [role="tabpanel"].is-selected {
display: block;
}
The first tab should be shown, while the second and third tabs are hidden. We’ll hide the second and third tabs with the hidden
attribute.
<div class="tab-contents">
<section id="digimon" role="tabpanel" ... > <!-- ... --> </section>
<section id="pokemon" role="tabpanel" hidden ... > <!-- ... --> </section>
<section id="tamagotchi" role="tabpanel" hidden ... > <!-- ... --> </section>
</div>
We also need to change the JavaScript to use the hidden
attribute. Here are the required changes:
const selectTab = tab => {
// ...
// Change this
tabPanels.forEach(c => c.classList.remove('is-selected'))
tabPanel.classList.add('is-selected')
// To this
tabPanels.forEach(c => c.setAttribute('hidden', 'true'))
tabPanel.removeAttribute('hidden')
}
Further simplifying the Tabpanel
HTML for a Tabpanel looks like this right now:
<section
role="tabpanel"
id="digimon"
data-theme="digimon"
aria-labelledby="digimon-title"
tabindex="-1"
>
<!-- ... -->
<h2 id="digimon-title">Digimon </h2>
<!-- ... -->
</section>
We can remove digimon-title
if we bring the id
from role="tabpanel"
into the <h2>
. The refactored HTML looks like this:
<section
role="tabpanel"
data-theme="digimon"
aria-labelledby="digimon"
tabindex="-1"
>
<!-- ... -->
<h2 id="digimon">Digimon </h2>
<!-- ... -->
</section>
To do this, we need to change how we find the tab panel in the JavaScript.
// Change this
const tabPanel = tabby.querySelector('#' + target)
// To this
const tabPanel = tabby.querySelector('#' + target).parentElement
getTabpanel
Did you notice we had to write the code to get tab panels twice? It makes sense to create a function to get the tab panel. We’ll call it getTabpanel
.
const tabPanel = tabby.querySelector('#' + target).parentElement
It makes sense to create a function to get the tab panel.
const getTabpanel = id => {
return tabby.querySelector('#' + id).parentElement
}
Using getTabpanel
:
const target = tab.dataset.target
const tabPanel = getTabpanel(target)
That’s it!