preline
Version:
Preline UI is an open-source set of prebuilt UI components based on the utility-first Tailwind CSS framework.
369 lines (310 loc) • 9.71 kB
text/typescript
/*
* HSTabs
* @version: 3.2.2
* @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, ITabsOnChangePayload, ITabsOptions } from "./interfaces";
import HSBasePlugin from "../base-plugin";
import { ICollectionItem } from "../../interfaces";
import { IAccessibilityComponent } from "../accessibility-manager/interfaces";
import HSAccessibilityObserver from "../accessibility-manager";
import { BREAKPOINTS } from "../../constants";
class HSTabs extends HSBasePlugin<ITabsOptions> implements ITabs {
private accessibilityComponent: IAccessibilityComponent;
private readonly eventType: "click" | "hover";
private readonly preventNavigationResolution: string | number | null;
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;
private onToggleHandler:
| {
el: HTMLElement;
fn: (evt: Event) => void;
preventClickFn: (evt: Event) => void | null;
}[]
| null;
private onExtraToggleChangeListener: (evt: Event) => void;
constructor(el: HTMLElement, options?: ITabsOptions, events?: {}) {
super(el, options, events);
const data = el.getAttribute("data-hs-tabs");
const dataOptions: ITabsOptions = data ? JSON.parse(data) : {};
const concatOptions = {
...dataOptions,
...options,
};
this.eventType = concatOptions.eventType ?? "click";
this.preventNavigationResolution =
typeof concatOptions.preventNavigationResolution === "number"
? concatOptions.preventNavigationResolution
: BREAKPOINTS[concatOptions.preventNavigationResolution] || null;
this.toggles = this.el.querySelectorAll("[data-hs-tab]");
this.extraToggleId = this.el.getAttribute("data-hs-tab-select");
this.extraToggle = this.extraToggleId
? (document.querySelector(this.extraToggleId) as HTMLSelectElement)
: null;
this.current = Array.from(this.toggles).find((el) =>
el.classList.contains("active")
);
this.currentContentId = this.current?.getAttribute("data-hs-tab") || null;
this.currentContent = this.currentContentId
? document.querySelector(this.currentContentId)
: null;
this.prev = null;
this.prevContentId = null;
this.prevContent = null;
this.onToggleHandler = [];
this.init();
}
private toggle(el: HTMLElement) {
this.open(el);
}
private extraToggleChange(evt: Event) {
this.change(evt);
}
private init() {
this.createCollection(window.$hsTabsCollection, this);
this.toggles.forEach((el) => {
const fn = (evt: Event) => {
if (
this.eventType === "click" && this.preventNavigationResolution &&
document.body.clientWidth <= +this.preventNavigationResolution
) evt.preventDefault();
this.toggle(el);
};
const preventClickFn = (evt: Event) => {
if (
this.preventNavigationResolution &&
document.body.clientWidth <= +this.preventNavigationResolution
) evt.preventDefault();
};
this.onToggleHandler.push({ el, fn, preventClickFn });
if (this.eventType === "click") el.addEventListener("click", fn);
else {
el.addEventListener("mouseenter", fn);
el.addEventListener("click", preventClickFn);
}
});
if (this.extraToggle) {
this.onExtraToggleChangeListener = (evt) => this.extraToggleChange(evt);
this.extraToggle.addEventListener(
"change",
this.onExtraToggleChangeListener,
);
}
if (typeof window !== "undefined") {
if (!window.HSAccessibilityObserver) {
window.HSAccessibilityObserver = new HSAccessibilityObserver();
}
this.setupAccessibility();
}
}
private open(el: HTMLElement) {
this.prev = this.current;
this.prevContentId = this.currentContentId;
this.prevContent = this.currentContent;
this.current = el;
this.currentContentId = el.getAttribute("data-hs-tab");
this.currentContent = this.currentContentId
? document.querySelector(this.currentContentId)
: null;
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,
tabsId: this.el.id,
} as ITabsOnChangePayload);
dispatch("change.hs.tab", el, {
el,
prev: this.prevContentId,
current: this.currentContentId,
tabsId: this.el.id,
} as ITabsOnChangePayload);
}
private change(evt: Event) {
const toggle: HTMLElement = document.querySelector(
`[data-hs-tab="${(evt.target as HTMLSelectElement).value}"]`,
);
if (toggle) {
if (this.eventType === "hover") {
toggle.dispatchEvent(new Event("mouseenter"));
} else toggle.click();
}
}
// Accessibility methods
private setupAccessibility(): void {
this.accessibilityComponent = window.HSAccessibilityObserver
.registerComponent(
this.el,
{
onArrow: (evt: KeyboardEvent) => {
if (evt.metaKey) return;
const isVertical =
this.el.getAttribute("data-hs-tabs-vertical") === "true" || this.el.getAttribute("aria-orientation") === "vertical";
switch (evt.key) {
case isVertical ? "ArrowUp" : "ArrowLeft":
this.onArrow(true);
break;
case isVertical ? "ArrowDown" : "ArrowRight":
this.onArrow(false);
break;
case "Home":
this.onStartEnd(true);
break;
case "End":
this.onStartEnd(false);
break;
}
},
},
true,
"Tabs",
'[role="tablist"]',
);
}
private onArrow(isOpposite = true) {
const toggles = isOpposite
? Array.from(this.toggles).reverse()
: Array.from(this.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();
}
private onStartEnd(isOpposite = true) {
const toggles = isOpposite
? Array.from(this.toggles)
: Array.from(this.toggles).reverse();
if (toggles.length) {
toggles[0].focus();
toggles[0].click();
}
}
// Public methods
public destroy() {
this.toggles.forEach((toggle) => {
const _toggle = this.onToggleHandler?.find(({ el }) => el === toggle);
if (_toggle) {
if (this.eventType === "click") {
toggle.removeEventListener("click", _toggle.fn);
} else {
toggle.removeEventListener("mouseenter", _toggle.fn);
toggle.removeEventListener("click", _toggle.preventClickFn);
}
}
});
this.onToggleHandler = [];
if (this.extraToggle) {
this.extraToggle.removeEventListener(
"change",
this.onExtraToggleChangeListener,
);
}
if (typeof window !== "undefined" && window.HSAccessibilityObserver) {
window.HSAccessibilityObserver.unregisterComponent(
this.accessibilityComponent,
);
}
window.$hsTabsCollection = window.$hsTabsCollection.filter(({ element }) =>
element.el !== this.el
);
}
// 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 = [];
}
if (window.$hsTabsCollection) {
window.$hsTabsCollection = window.$hsTabsCollection.filter(
({ element }) => document.contains(element.el),
);
}
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);
}
});
}
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 = elInCollection
? Array.from(elInCollection.element.toggles).find(
(el) =>
el ===
(typeof target === "string"
? document.querySelector(target)
: target),
)
: null;
if (
targetInCollection && !targetInCollection.classList.contains("active")
) {
elInCollection.element.open(targetInCollection);
}
}
// 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: typeof HSTabs;
$hsTabsCollection: ICollectionItem<HSTabs>[];
}
}
window.addEventListener("load", () => {
HSTabs.autoInit();
});
if (typeof window !== "undefined") {
window.HSTabs = HSTabs;
}
export default HSTabs;