Skip to content

Instantly share code, notes, and snippets.

@dombarnes
Last active September 4, 2024 17:15
Show Gist options
  • Save dombarnes/a69878c93f6e91807f3f6f19e1107052 to your computer and use it in GitHub Desktop.
Save dombarnes/a69878c93f6e91807f3f6f19e1107052 to your computer and use it in GitHub Desktop.
TabbedView Stimulus Controller, inspired by Flowbite
// Graciously provided by:
// https://flowbite.com/docs/components/tabs/#default-tabs
// Converted to Stimulus.js and enhanced with Cursor by Dom Barnes @domster
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'panel', 'tab' ]
connect() {
// this.index = 0
this.index = this.tabTargets.indexOf(document.querySelector('[aria-current=page'))
const url = new URL(window.location.toString())
if (url.searchParams.has('tab')) {
// load the tab from the url
const tab = this.tabTargets.find((tab) => tab.dataset.tabId === url.searchParams.get('tab'))
this.index = this.tabTargets.indexOf(tab)
}
this.focusTab()
this.tabTargets.forEach((tab, index) => {
tab.setAttribute('role', 'tab')
tab.setAttribute('type', 'button')
tab.setAttribute('aria-current', index === this.index ? 'page' : null)
tab.setAttribute('aria-selected', index === this.index ? 'true' : 'false')
tab.setAttribute('tabindex', index === this.index ? '0' : '-1')
tab.classList.add(...this.DefaultClassList)
const panel = this.panelTargets[index]
panel.setAttribute('role', 'tabpanel')
panel.setAttribute('aria-labelledby', tab.id)
panel.setAttribute('tabindex', '0')
this.refreshTabClasses(tab)
tab.addEventListener('keydown', this.handleKeydown.bind(this))
})
}
selectTab(event) {
event.target.ariaCurrent = 'page'
const panelId = event.target.dataset.tabId
this.tabTargets.forEach((tab) => {
if (tab.dataset.tabId != panelId) {
tab.removeAttribute('aria-current')
}
const isSelected = tab.dataset.tabId === panelId
tab.setAttribute('aria-selected', isSelected ? 'true' : 'false')
tab.setAttribute('tabindex', isSelected ? '0' : '-1')
// Remove active style classes and apply inactive accordingly
this.refreshTabClasses(tab)
})
this.panelTargets.forEach((panel) => {
panel.hidden = panel.dataset.tab !== panelId
})
// Push the tab change to the URL
const url = new URL(window.location.toString())
url.searchParams.set('tab', panelId)
history.replaceState({}, '', url)
}
refreshTabClasses(element) {
if (element.hasAttribute('aria-current') && element.getAttribute('aria-current') == 'page') {
element.classList.add(...this.ActiveClassList)
element.classList.remove(...this.InactiveClassList)
} else {
element.classList.remove(...this.ActiveClassList)
element.classList.add(...this.InactiveClassList)
}
}
handleKeydown(event) {
switch (event.key) {
case 'ArrowLeft':
this.index = (this.index - 1 + this.tabTargets.length) % this.tabTargets.length;
this.focusTab();
break;
case 'ArrowRight':
this.index = (this.index + 1) % this.tabTargets.length;
this.focusTab();
break;
case 'Home':
this.index = 0;
this.focusTab();
break;
case 'End':
this.index = this.tabTargets.length - 1;
this.focusTab();
break;
}
}
focusTab() {
const tab = this.tabTargets[this.index];
tab.focus();
this.selectTab({ target: tab });
}
ActiveClassList = [
'border-b-2',
'!text-white',
'hover:!text-white',
'dark:text-blue-500',
'dark:hover:text-blue-500',
'border-blue-500',
'dark:border-blue-500',
'bg-teal-600',
]
InactiveClassList = [
'border',
'border-solid',
'border-grey-modern-300',
'hover:border-blue-500',
'dark:border-transparent',
'text-gray-500',
'hover:text-white',
'dark:text-gray-400',
'hover:border-gray-300',
'dark:border-gray-700',
'dark:hover:text-white-100',
'hover:bg-blue-500'
]
DefaultClassList = [
'mt-2',
'cursor-pointer',
'rounded-t-lg',
'inline-block',
'p-4',
'mb-0',
]
}
<div data-controller="tabbed-view">
<ul id="tab-list" class="flex flex-wrap -mb-px text-sm font-medium text-center" role="tablist">
<li role="presentation">
<a data-tabbed-view-target="tab" data-tab-id="tab-one" aria-current="page" aria-controls="tab-one-panel" data-action="click->tabbed-view#selectTab">Tab One</a>
</li>
<li role="presentation">
<a data-tabbed-view-target="tab" data-tab-id="tab-twp" aria-controls="tab-two-panel" data-action="click->tabbed-view#selectTab">Tab Two</a>
</li>
</ul>
</div>
<div class="border border-solid border-gray-200 dark:border-gray-700" id="tab-container">
<div class="p-5">
<div tabindex="-1" data-tabbed-view-target="panel" data-tab="tab-one">
<h2>Tab One</h2>
</div>
<div tabindex="-1" data-tabbed-view-target="panel" data-tab="system-players">
<h2>Tab Two</h2>
</div>
</div>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment