preline
Version:
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
253 lines (210 loc) • 6.85 kB
text/typescript
/*
* HSTabs
* @version: 2.5.0
* @author: Preline Labs Ltd.
* @license: Licensed under MIT and Preline UI Fair Use License (https://preline.co/docs/license.html)
* Copyright 2024 Preline Labs Ltd.
*/
import { dispatch } from '../../utils';
import { ITabs } from '../tabs/interfaces';
import HSBasePlugin from '../base-plugin';
import { ICollectionItem } from '../../interfaces';
import { TABS_ACCESSIBILITY_KEY_SET } from '../../constants';
class HSTabs extends HSBasePlugin<{}> implements ITabs {
public toggles: NodeListOf<HTMLElement> | null;
private readonly extraToggleId: string | null;
private readonly extraToggle: HTMLSelectElement | null;
private current: HTMLElement | null;
private currentContentId: string | null;
public currentContent: HTMLElement | null;
private prev: HTMLElement | null;
private prevContentId: string | null;
private prevContent: HTMLElement | null;
constructor(el: HTMLElement, options?: {}, events?: {}) {
super(el, options, events);
this.toggles = this.el.querySelectorAll('[data-hs-tab]');
this.extraToggleId = this.el.getAttribute('data-hs-tab-select');
this.extraToggle = document.querySelector(this.extraToggleId);
this.current = Array.from(this.toggles).find((el) =>
el.classList.contains('active'),
);
this.currentContentId = this.current.getAttribute('data-hs-tab');
this.currentContent = document.querySelector(this.currentContentId);
this.prev = null;
this.prevContentId = null;
this.prevContent = null;
this.init();
}
private init() {
this.createCollection(window.$hsTabsCollection, this);
this.toggles.forEach((el) => {
el.addEventListener('click', () => this.open(el));
});
if (this.extraToggle) {
this.extraToggle.addEventListener('change', (evt) => this.change(evt));
}
}
private open(el: HTMLElement) {
this.prev = this.current;
this.prevContentId = this.currentContentId;
this.prevContent = this.currentContent;
this.current = el;
this.currentContentId = this.current.getAttribute('data-hs-tab');
this.currentContent = document.querySelector(this.currentContentId);
if (this?.prev?.ariaSelected) this.prev.ariaSelected = 'false';
this.prev.classList.remove('active');
this.prevContent.classList.add('hidden');
if (this?.current?.ariaSelected) this.current.ariaSelected = 'true';
this.current.classList.add('active');
this.currentContent.classList.remove('hidden');
this.fireEvent('change', {
el,
prev: this.prevContentId,
current: this.currentContentId,
});
dispatch('change.hs.tab', el, {
el,
prev: this.prevContentId,
current: this.currentContentId,
});
}
private change(evt: Event) {
const toggle: HTMLElement = document.querySelector(
`[data-hs-tab="${(evt.target as HTMLSelectElement).value}"]`,
);
if (toggle) toggle.click();
}
// Static methods
static getInstance(target: HTMLElement | string, isInstance?: boolean) {
const elInCollection = window.$hsTabsCollection.find(
(el) =>
el.element.el ===
(typeof target === 'string' ? document.querySelector(target) : target),
);
return elInCollection
? isInstance
? elInCollection
: elInCollection.element
: null;
}
static autoInit() {
if (!window.$hsTabsCollection) window.$hsTabsCollection = [];
document
.querySelectorAll(
'[role="tablist"]:not(select):not(.--prevent-on-load-init)',
)
.forEach((el: HTMLElement) => {
if (
!window.$hsTabsCollection.find(
(elC) => (elC?.element?.el as HTMLElement) === el,
)
)
new HSTabs(el);
});
if (window.$hsTabsCollection)
document.addEventListener('keydown', (evt) => HSTabs.accessibility(evt));
}
static open(target: HTMLElement) {
const elInCollection = window.$hsTabsCollection.find((el) =>
Array.from(el.element.toggles).includes(
typeof target === 'string' ? document.querySelector(target) : target,
),
);
const targetInCollection = Array.from(elInCollection.element.toggles).find(
(el) =>
el ===
(typeof target === 'string' ? document.querySelector(target) : target),
);
if (targetInCollection && !targetInCollection.classList.contains('active'))
elInCollection.element.open(targetInCollection);
}
// Accessibility methods
static accessibility(evt: KeyboardEvent) {
const target = document.querySelector('[data-hs-tab]:focus');
if (
target &&
TABS_ACCESSIBILITY_KEY_SET.includes(evt.code) &&
!evt.metaKey
) {
const isVertical = target
.closest('[role="tablist"]')
.getAttribute('data-hs-tabs-vertical');
evt.preventDefault();
switch (evt.code) {
case isVertical === 'true' ? 'ArrowUp' : 'ArrowLeft':
this.onArrow();
break;
case isVertical === 'true' ? 'ArrowDown' : 'ArrowRight':
this.onArrow(false);
break;
case 'Home':
this.onStartEnd();
break;
case 'End':
this.onStartEnd(false);
break;
default:
break;
}
}
}
static onArrow(isOpposite = true) {
const target = document
.querySelector('[data-hs-tab]:focus')
.closest('[role="tablist"]');
const targetInCollection = window.$hsTabsCollection.find(
(el) => el.element.el === target,
);
if (targetInCollection) {
const toggles = isOpposite
? Array.from(targetInCollection.element.toggles).reverse()
: Array.from(targetInCollection.element.toggles);
const focused = toggles.find((el) => document.activeElement === el);
let focusedInd = toggles.findIndex((el: any) => el === focused);
focusedInd = focusedInd + 1 < toggles.length ? focusedInd + 1 : 0;
toggles[focusedInd].focus();
toggles[focusedInd].click();
}
}
static onStartEnd(isOpposite = true) {
const target = document
.querySelector('[data-hs-tab]:focus')
.closest('[role="tablist"]');
const targetInCollection = window.$hsTabsCollection.find(
(el) => el.element.el === target,
);
if (targetInCollection) {
const toggles = isOpposite
? Array.from(targetInCollection.element.toggles)
: Array.from(targetInCollection.element.toggles).reverse();
if (toggles.length) {
toggles[0].focus();
toggles[0].click();
}
}
}
// Backward compatibility
static on(evt: string, target: HTMLElement, cb: Function) {
const elInCollection = window.$hsTabsCollection.find((el) =>
Array.from(el.element.toggles).includes(
typeof target === 'string' ? document.querySelector(target) : target,
),
);
if (elInCollection) elInCollection.element.events[evt] = cb;
}
}
declare global {
interface Window {
HSTabs: Function;
$hsTabsCollection: ICollectionItem<HSTabs>[];
}
}
window.addEventListener('load', () => {
HSTabs.autoInit();
// Uncomment for debug
// console.log('Tabs collection:', window.$hsTabsCollection);
});
if (typeof window !== 'undefined') {
window.HSTabs = HSTabs;
}
export default HSTabs;