web/wp-content/themes/mozilla-builders/static/js/plugins/tabs.js (227 lines of code) (raw):
/**
* `x-tabs` is a set of layered sections of content known
* as tab panels that are displayed one at a time.
*
* @param {import('alpinejs').Alpine} Alpine
*/
export function tabs(Alpine) {
Alpine.directive('tabs', (el, { value, modifiers, expression }, { evaluate }) => {
if (value === 'list') {
handleList(el, Alpine, {
loop: modifiers.includes('loop'),
automatic: modifiers.includes('automatic'),
orientation: modifiers.includes('vertical') ? 'vertical' : 'horizontal',
});
} else if (value === 'tab') {
handleTab(el, Alpine, { value: expression });
} else if (value === 'panel') {
handlePanel(el, Alpine, { value: expression });
} else {
handleRoot(el, Alpine, evaluate(expression));
}
});
}
/**
* @param {HTMLElement} el
* @param {import('alpinejs').Alpine} Alpine
* @param {{ default: string }} config
*/
function handleRoot(el, Alpine, config) {
let didRender = false;
Alpine.bind(el, {
'x-data'() {
return {
__root: el,
tabs: [],
defaultTab: config?.default || null,
activeTab: config?.initial || null,
};
},
'x-id'() {
return ['tb-tabs-tab', 'tb-tabs-panel'];
},
'x-effect'() {
// Update query params
if (this.activeTab) {
const params = new URLSearchParams(window.location.search);
params.set('tab', this.activeTab);
if (this.activeTab === this.defaultTab) {
params.delete('tab');
}
const pathname = window.location.pathname;
const search = params.size > 0 ? `?${params.toString()}` : '';
window.history.replaceState(null, '', `${pathname}${search}`);
if (!didRender) {
didRender = true;
} else {
window.scrollTo({ top: el.offsetTop, behavior: 'smooth' });
}
}
},
});
}
/**
* @param {HTMLElement} el
* @param {import('alpinejs').Alpine} Alpine
* @param {{ loop: boolean, automatic: boolean, orientation: 'horizontal' | 'vertical' }} config
*/
function handleList(el, Alpine, config) {
Alpine.bind(el, {
'x-init'() {
if (!this.__root) {
console.error('x-tabs:tab must be placed inside an x-tabs.', el);
}
},
'x-data'() {
return {
__list: el,
loop: config.loop || false,
automatic: config.automatic || false,
orientation: config.orientation || 'horizontal',
};
},
role: 'tablist',
tabindex: 0,
});
}
/**
* @param {HTMLElement} el
* @param {import('alpinejs').Alpine} Alpine
* @param {{ value: string }} config
*/
function handleTab(el, Alpine, config) {
if (!config.value) {
return console.error('x-tabs:tab must have a value.', el);
}
Alpine.bind(el, {
'x-data'() {
return {
value: config.value,
};
},
'x-init'() {
if (!this.__list) {
console.error('x-tabs:tab must be placed inside an x-tabs:list.', el);
}
if (el.tagName !== 'BUTTON') {
console.error('x-tabs:tab must be a <button> element.', el);
}
// Assume this tab is the first if there is no active tab.
if (!this.activeTab) {
this.activeTab = this.value;
}
this.tabs.push(el);
},
':id'() {
return `${this.$id('tb-tabs-tab')}-${this.value}`;
},
role: 'tab',
':tabindex'() {
return this.activeTab === this.value ? '0' : -1;
},
':aria-selected'() {
return this.activeTab === this.value;
},
':aria-controls'() {
return `${this.$id('tb-tabs-panel')}-${this.value}`;
},
':data-state'() {
return this.activeTab === this.value ? 'active' : 'inactive';
},
'@click'() {
this.activeTab = this.value;
},
'@keydown.home.prevent.stop'() {
const tab = this.tabs.at(0);
if (tab) {
tab.focus();
}
if (tab && this.automatic) {
tab.click();
}
},
'@keydown.end.prevent.stop'() {
const tab = this.tabs.at(-1);
if (tab) {
tab.focus();
}
if (tab && this.automatic) {
tab.click();
}
},
'@keydown.left.prevent.stop'() {
if (this.orientation === 'horizontal') {
const index = this.tabs.indexOf(el);
if (index >= 0) {
const next = this.loop ? index - 1 : Math.max(index - 1, 0);
const tab = this.tabs.at(next);
if (tab) {
tab.focus();
}
if (tab && this.automatic) {
tab.click();
}
}
}
},
'@keydown.right.prevent.stop'() {
if (this.orientation === 'horizontal') {
const index = this.tabs.indexOf(el);
if (index >= 0) {
const previous = this.loop
? (index + 1) % this.tabs.length
: Math.min(index + 1, this.tabs.length - 1);
const tab = this.tabs.at(previous);
if (tab) {
tab.focus();
}
if (tab && this.automatic) {
tab.click();
}
}
}
},
'@keydown.up.prevent.stop'() {
if (this.orientation === 'vertical') {
const index = this.tabs.indexOf(el);
if (index >= 0) {
const next = this.loop ? index - 1 : Math.max(index - 1, 0);
const tab = this.tabs.at(next);
if (tab) {
tab.focus();
}
if (tab && this.automatic) {
tab.click();
}
}
}
},
'@keydown.down.prevent.stop'() {
if (this.orientation === 'vertical') {
const index = this.tabs.indexOf(el);
if (index >= 0) {
const previous = this.loop
? (index + 1) % this.tabs.length
: Math.min(index + 1, this.tabs.length - 1);
const tab = this.tabs.at(previous);
if (tab) {
tab.focus();
}
if (tab && this.automatic) {
tab.click();
}
}
}
},
});
}
/**
* @param {HTMLElement} el
* @param {import('alpinejs').Alpine} Alpine
* @param {{ value: string }} config
*/
function handlePanel(el, Alpine, config) {
if (!config.value) {
return console.error('x-tabs:panel must have a value.', el);
}
Alpine.bind(el, {
'x-init'() {
if (!this.__root) {
console.error('x-tabs:panel must be placed inside an x-tabs', el);
}
},
'x-data'() {
return {
value: config.value,
};
},
':id'() {
return `${this.$id('tb-tabs-panel')}-${this.value}`;
},
role: 'tabpanel',
':tabindex'() {
return this.activeTab === this.value ? '0' : -1;
},
':data-state'() {
return this.activeTab === this.value ? 'active' : 'inactive';
},
':aria-labelledby'() {
return `${this.$id('tb-tabs-tab')}-${this.value}`;
},
'x-show'() {
return this.activeTab === this.value;
},
});
}