Last active
September 4, 2024 17:15
-
-
Save dombarnes/a69878c93f6e91807f3f6f19e1107052 to your computer and use it in GitHub Desktop.
TabbedView Stimulus Controller, inspired by Flowbite
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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', | |
] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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