🛠️ Tabby: Refactor

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

🛠️ 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:

  1. The is-selected class
  2. 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:

  1. Use data-selected="true" instead of aria-selected="true". The styles are similar.
  2. 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!