@miyagi/core
Version:
miyagi is a component development tool for JavaScript template engines.
404 lines (331 loc) • 8.21 kB
JavaScript
customElements.define(
"accordion-tabs",
class AccordionTabs extends HTMLElement {
#accordion;
#breakpoint;
#prevMode;
#resizeObserver;
#tabs;
static get observedAttributes() {
return ["breakpoint", "current"];
}
constructor() {
super();
}
connectedCallback() {
if (this.closest("code")) return;
this.content = [];
if (this.hasAttribute("breakpoint")) {
const breakpoint = this.getAttribute("breakpoint");
const bpParsed = parseInt(breakpoint, 10);
let result;
if (breakpoint.endsWith("rem")) {
result =
bpParsed *
parseInt(
window.getComputedStyle(document.documentElement).fontSize,
10,
);
} else if (breakpoint.endsWith("em")) {
result =
bpParsed * parseInt(window.getComputedStyle(this).fontSize, 10);
} else {
result = bpParsed;
}
this.#breakpoint = result;
}
window.requestAnimationFrame(() => {
this.details = Array.from(this.children);
this.details.forEach((child, i) => {
const title = child.querySelector("summary");
if (child.open) {
this.index = i;
}
this.content.push({
title: title.textContent,
content: [...title.parentElement.children].filter(
(el) => el.nodeType === 1 && el !== title,
),
});
});
let initialized = false;
this.#resizeObserver = new ResizeObserver((entries) => {
if (initialized) {
for (const entry of entries) {
this.#render(entry.borderBoxSize[0].inlineSize);
}
}
initialized = true;
});
this.#render(this.clientWidth, () => {
this.#resizeObserver.observe(this);
});
});
}
attributeChangedCallback(attr, oldValue, newValue) {
if (attr === "current") {
this.index = parseInt(newValue, 10);
this.#render(this.clientWidth);
}
}
disconnectedCallback() {
if (this.#resizeObserver) {
this.#resizeObserver.disconnect();
}
}
async #render(width, cb) {
if (!this.#breakpoint || width < this.#breakpoint) {
await this.#renderAccordion();
this.#prevMode = "accordion";
} else {
await this.#renderTabs();
this.#prevMode = "tabs";
}
if (cb) {
cb();
}
}
#renderAccordion() {
if (!this.#accordion) {
this.#accordion = new AccordionTabsAccordion(this);
}
if (this.#prevMode === "tabs") {
this.#clear();
this.#accordion.setElements();
this.#accordion.elements.forEach((detail) => this.appendChild(detail));
} else {
this.#accordion.render();
}
return true;
}
async #renderTabs() {
this.#clear();
if (this.#tabs) {
this.#tabs.index = typeof this.index === "number" ? this.index : 0;
this.#tabs.setElements();
} else {
this.#tabs = new AccordionTabsTabs(this);
}
const [ol, content] = this.#tabs.elements;
this.appendChild(ol);
content.forEach((item) => {
this.appendChild(item);
});
return await this.#tabs.render(false);
}
#clear() {
Array.from(this.children).forEach((child) => this.removeChild(child));
}
},
);
class AccordionTabsAccordion {
constructor(AccordionTabs) {
this.AccordionTabs = AccordionTabs;
this.elements = this.AccordionTabs.details;
this.elements.forEach((detail, i) => {
detail
.querySelector("summary")
.addEventListener("click", ({ target }) => {
requestAnimationFrame(() => {
this.#onToggle(target.closest("details"), i);
});
});
});
this.render();
}
setElements() {
this.elements.forEach((detail, i) => {
this.AccordionTabs.content[i].content.forEach((item) =>
detail.appendChild(item),
);
detail.open = i === this.AccordionTabs.index;
});
}
render() {
this.elements.forEach((el, i) => {
el.open = i === this.AccordionTabs.index;
});
}
#onToggle(element, i) {
if (element.open) {
this.elements.forEach((el) => {
if (element !== el) {
el.open = false;
}
});
this.AccordionTabs.index = i;
} else {
this.AccordionTabs.index = null;
}
}
}
class AccordionTabsTabs {
#AccordionTabs;
#buttons = [];
#divs = [];
#TabsList;
constructor(AccordionTabs) {
this.#AccordionTabs = AccordionTabs;
this.elements = this.getElements();
this.index =
typeof this.#AccordionTabs.index === "number"
? this.#AccordionTabs.index
: 0;
this.#TabsList = new AccordionTabsList(this);
}
getElements() {
const ol = document.createElement("ol");
const arr = [];
this.#AccordionTabs.content.forEach(({ title, content }, i) => {
const button = document.createElement("button");
const li = document.createElement("li");
const div = document.createElement("div");
const id = crypto.randomUUID();
const idTab = `tab-${id}`;
const idPanel = `panel-${id}`;
ol.setAttribute("role", "tablist");
li.setAttribute("role", "presentation");
button.textContent = title;
button.type = "button";
button.id = idTab;
button.setAttribute("aria-selected", i === this.index ? "true" : "false");
button.setAttribute("tabindex", i === this.index ? 0 : -1);
button.setAttribute("aria-controls", idPanel);
button.setAttribute("role", "tab");
this.#buttons.push(button);
li.appendChild(button);
ol.appendChild(li);
content.forEach((item) => {
div.appendChild(item);
});
div.id = idPanel;
div.hidden = this.index !== i;
div.setAttribute("role", "tabpanel");
div.setAttribute("tabindex", "0");
div.setAttribute("aria-labelledby", idTab);
arr.push(div);
this.#divs.push(div);
});
return [ol, arr];
}
setElements() {
this.elements[1].forEach((div, i) => {
this.#AccordionTabs.content[i].content.forEach((item) =>
div.appendChild(item),
);
});
}
/**
* @param {number} activeTab
*/
setActiveTab(activeTab) {
this.#AccordionTabs.index = this.index = activeTab;
this.render();
}
/**
* @returns {void}
*/
async render(focus = true) {
this.elements[1].forEach((tabpanel, i) => {
tabpanel.hidden = i !== this.index;
});
return await this.#TabsList.render(focus);
}
}
class AccordionTabsList {
#Tabs;
#elements;
/**
* @param {object} Tabs
*/
constructor(Tabs) {
this.#Tabs = Tabs;
this.#elements = Array.from(
this.#Tabs.elements[0].querySelectorAll("button"),
);
this.#elements.forEach((button) => {
button.addEventListener("click", this.#onClick.bind(this));
button.addEventListener("keydown", this.#onKeydown.bind(this));
});
}
/**
* @param {Event} object
* @param {HTMLButtonElement} object.currentTarget
*/
#onClick({ currentTarget }) {
this.#Tabs.setActiveTab(this.#elements.indexOf(currentTarget));
}
/**
* @param {Event} event
*/
#onKeydown(event) {
const { dir } = event.target.closest("[dir]") || document.documentElement;
let flag = false;
switch (event.key) {
case "ArrowLeft":
if (dir === "rtl") {
this.#setNextTab();
} else {
this.#setPreviousTab();
}
flag = true;
break;
case "ArrowRight":
if (dir === "rtl") {
this.#setPreviousTab();
} else {
this.#setNextTab();
}
flag = true;
break;
case "Home":
this.#Tabs.setActiveTab(0);
flag = true;
break;
case "End":
this.#Tabs.setActiveTab(this.#elements.length - 1);
flag = true;
break;
default:
break;
}
if (flag) {
event.stopPropagation();
event.preventDefault();
}
}
/**
* @returns {void}
*/
#setNextTab() {
this.#Tabs.setActiveTab(
this.#Tabs.index === this.#elements.length - 1 ? 0 : this.#Tabs.index + 1,
);
}
/**
* @returns {void}
*/
#setPreviousTab() {
this.#Tabs.setActiveTab(
this.#Tabs.index === 0 ? this.#elements.length - 1 : this.#Tabs.index - 1,
);
}
/**
* @returns {void}
*/
render(focus = true) {
this.#elements.forEach((button, i) => {
if (i === this.#Tabs.index) {
button.setAttribute("aria-selected", "true");
button.removeAttribute("tabindex");
if (focus) {
button.focus();
}
} else {
button.setAttribute("aria-selected", "false");
button.setAttribute("tabindex", -1);
}
});
return new Promise((resolve) => setTimeout(resolve, 1000));
}
}