wj-elements
Version:
WebJET Elements is a modern set of user interface tools harnessing the power of web components designed to simplify web application development.
386 lines (385 loc) • 15.8 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import WJElement from "./wje-element.js";
const styles = "/*\n[ WJ Tab Group ]\n*/\n\n:host {\n --wje-tab-top: 0;\n --wje-tab-start: 0;\n --wje-tab-end: 0;\n --wje-tab-bottom: 0;\n width: 100%;\n}\n.native-tab-group {\n display: flex;\n flex-direction: column;\n\n overflow: hidden;\n position: relative;\n}\n\n.native-tab-group > header {\n display: flex;\n flex-direction: column;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n\n & > nav {\n display: flex;\n align-items: center;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n overflow: hidden;\n }\n}\n\n.native-tab-group > section {\n width: 100%;\n\n & > article {\n scroll-snap-align: start;\n overflow-y: auto;\n overscroll-behavior-y: contain;\n }\n}\n\n/*TOP*/\n:host([variant='top']) {\n --wje-tab-top: auto !important;\n --wje-tab-writing-mode: horizontal-tb;\n .native-tab-group {\n flex-direction: column;\n }\n nav {\n border-bottom: var(--wje-tab-group-nav-border);\n }\n}\n\n/*START*/\n:host([variant='start']) {\n --wje-tab-start: auto !important;\n --wje-tab-writing-mode: vertical-rl;\n .native-tab-group {\n flex-direction: row;\n }\n nav {\n flex-direction: column;\n border-right: var(--wje-tab-group-nav-border);\n }\n}\n\n/*END*/\n:host([variant='end']) {\n --wje-tab-writing-mode: vertical-rl;\n .native-tab-group {\n flex-direction: row-reverse;\n }\n nav {\n flex-direction: column;\n border-left: var(--wje-tab-group-nav-border);\n }\n}\n\n/*BOTTOM*/\n:host([variant='bottom']) {\n --wje-tab-bottom: auto !important;\n --wje-tab-writing-mode: horizontal-tb;\n .native-tab-group {\n flex-direction: column-reverse;\n }\n nav {\n border-top: var(--wje-tab-group-nav-border);\n }\n}\n\n.dropdown-active {\n wje-button{\n &::part(native) {\n color: var(--wje-color-primary-8);\n }\n }\n}\n\n.more-tabs {\n flex: 0 0 auto;\n}\n";
const _TabGroup = class _TabGroup extends WJElement {
/**
* Creates an instance of TabGroup.
* @class
*/
constructor() {
super();
__publicField(this, "className", "TabGroup");
this._instanceId = ++_TabGroup._instanceId;
this._lastNavWidth = null;
this._initialized = false;
this._moreWidth = 0;
}
/**
* Sets the value for the 'variant' attribute of the element.
* @param {string} value The value to set for the 'variant' attribute.
*/
set variant(value) {
this.setAttribute("variant", value);
this.setAriaState({
orientation: value === "start" || value === "end" ? "vertical" : "horizontal"
});
}
/**
* Gets the value of the 'variant' attribute.
* If the attribute is not set, it defaults to 'top'.
* @returns {string} The value of the 'variant' attribute or the default value 'top' if not set.
*/
get variant() {
return this.getAttribute("variant") || "top";
}
/**
* Sets the 'type' attribute of the element to the specified value.
* @param {string} value The value to set for the 'type' attribute.
*/
set type(value) {
this.setAttribute("type", value);
}
/**
* Retrieves the `type` attribute of the element.
* If the `type` attribute is not set, it defaults to `'panel'`.
* @returns {string} The value of the `type` attribute or the default value `'panel'`.
*/
get type() {
return this.getAttribute("type") || "panel";
}
/**
* Returns the CSS styles for the component.
* @static
* @returns {CSSStyleSheet}
*/
static get cssStyleSheet() {
return styles;
}
/**
* Sets up the attributes for the component.
*/
setupAttributes() {
this.isShadowRoot = "open";
this.setAriaState({
role: "tablist",
orientation: this.variant === "start" || this.variant === "end" ? "vertical" : "horizontal"
});
}
/**
* Sets up the event listeners before the component is drawn.
* This method is called before the component is drawn.
* It is used to set up event listeners.
*/
beforeDraw() {
let activeTabName = location.hash.replace("#", "");
if (this.getPanelAllName().includes(activeTabName)) {
window.addEventListener("load", (e) => {
this.setActiveTab(activeTabName);
});
}
}
/**
* Creates and returns a document fragment containing a structured layout for a tab group.
* The tab group layout includes a `header` section with navigational elements,
* a `section` element for tab panels, and slots for customization such as additional navigation items,
* dropdowns, and more.
* The structure comprises:
* - A `div` container with relevant styling and part attributes.
* - A `header` for tabs, including a slot for navigation (`nav`) and additional tabs in a dropdown (`moreDropdown`).
* - A `section` for tab panels with a customizable `slot`.
* This function also initializes the `nav` and `moreDropdown` properties for external use.
* @returns {DocumentFragment} The completed document fragment containing the tab group layout.
*/
draw() {
let fragment = document.createDocumentFragment();
let native = document.createElement("div");
native.setAttribute("part", "native");
native.classList.add("native-tab-group");
let header = document.createElement("header");
header.setAttribute("part", "tabs");
header.classList.add("scroll-snap-x");
let nav = document.createElement("nav");
nav.setAttribute("part", "nav");
let section = document.createElement("section");
section.setAttribute("part", "panels");
let slot = document.createElement("slot");
let slotNav = document.createElement("slot");
slotNav.setAttribute("name", "nav");
let icon = document.createElement("wje-icon");
icon.setAttribute("name", "dots");
let button = document.createElement("wje-button");
button.setAttribute("slot", "trigger");
button.setAttribute("fill", "link");
let menu = document.createElement("wje-menu");
menu.setAttribute("variant", "context");
let slotMore = document.createElement("slot");
slotMore.setAttribute("name", "more");
let moreDropdown = document.createElement("wje-dropdown");
moreDropdown.setAttribute("placement", "bottom-end");
moreDropdown.setAttribute("collapsible", "");
moreDropdown.classList.add("more-tabs");
button.append(icon);
menu.append(slotMore);
moreDropdown.append(button);
moreDropdown.append(menu);
header.append(nav);
nav.append(slotNav);
if (this.variant === "top" || this.variant === "bottom") {
nav.append(moreDropdown);
}
section.append(slot);
native.append(header);
native.append(section);
fragment.append(native);
this.nav = nav;
this.moreDropdown = moreDropdown;
return fragment;
}
/**
* Executes necessary initializations and attaches event listeners after a drawing operation.
* Handles active tab selection, 'wje-tab:change' event binding, and window resize event for overflow checking.
* @returns {void} Does not return a value.
*/
afterDraw() {
let activeTab = this.getActiveTab();
let activeTabName = activeTab ? activeTab[0][this.type] : this.getTabAll()[0][this.type];
this.setActiveTab(activeTabName);
this.addEventListener("wje-tab:change", (e) => {
if (e.detail.context.hasAttribute("disabled")) return;
this.setActiveTab(e.detail.context.panel);
});
if (this.variant === "top" || this.variant === "bottom") {
this.initTabMetrics();
this._resizeObserver = new ResizeObserver((entries) => {
const width = entries[0].contentRect.width;
if (width !== this._lastNavWidth) {
this._lastNavWidth = width;
this.checkOverflow();
}
});
this._resizeObserver.observe(this);
}
}
/**
* Removes the 'active' class from all panel and tab elements.
* @returns {void} This method does not return a value.
*/
removeActiveTab() {
this.getPanelAll().forEach((el) => {
el.classList.remove("active");
});
this.getTabAll().forEach((el) => {
el.classList.remove("active");
});
}
/**
* Sets the active tab and panel.
* @param {string} tab The name of the tab to set as active.
*/
setActiveTab(tab) {
var _a;
this.removeActiveTab();
const el = this.querySelector(`[${this.type}="${tab}"]`);
el == null ? void 0 : el.classList.add("active");
if (this.type === "panel")
(_a = this.querySelector(`[name="${tab}"]`)) == null ? void 0 : _a.classList.add("active");
if (el)
this.dropdownActive(el);
this.syncAria();
}
/**
* Returns the currently active tab.
* @returns {Element|null} The active tab, or null if no tab is active.
*/
getActiveTab() {
let activeTabs = Array.from(this.querySelectorAll("wje-tab.active"));
return activeTabs.length > 0 ? activeTabs : null;
}
/**
* Returns all tabs.
* @returns {Array<Element>} An array of all tabs.
*/
getTabAll() {
return this.context.querySelector('[name="nav"]').assignedElements();
}
/**
* Returns all tabs, including those moved to "more".
* @returns {Array<Element>} An array of all tabs.
*/
getAllTabs() {
return Array.from(this.querySelectorAll("wje-tab"));
}
/**
* Returns all panels.
* @returns {Array<Element>} An array of all panels.
*/
getPanelAll() {
return Array.from(this.querySelectorAll("wje-tab-panel"));
}
/**
* Returns the names of all tabs.
* @returns {Array<string>} An array of all tab names.
*/
getPanelAllName() {
return this.getPanelAll().map((el) => el.getAttribute("name"));
}
/**
* Toggles the visibility of the "more" dropdown based on the presence of tabs in the "more" slot.
* @returns {void} Does not return a value.
*/
toggleMoreVisibility() {
const hasTabsInMore = !!this.querySelector('wje-tab[slot="more"]');
const nextHidden = !hasTabsInMore;
if (this.moreDropdown.hidden !== nextHidden) {
this.moreDropdown.hidden = nextHidden;
}
}
/**
* Initializes metrics for tabs within the component. Assigns each tab to the navigation slot
* and calculates their dimensions for further operations.
* @returns {void} Does not return any value.
*/
initTabMetrics() {
requestAnimationFrame(() => {
this._tabMetrics = this.measureTabMetrics();
this.measureMoreWidth();
this._initialized = true;
this.checkOverflow();
this._lastNavWidth = this.nav.getBoundingClientRect().width;
});
}
/**
* Measures current tab widths while all tabs are temporarily placed into the main nav slot.
* @returns {Array<{el: Element, width: number}>}
*/
measureTabMetrics() {
const tabs = Array.from(this.querySelectorAll("wje-tab"));
tabs.forEach((tab) => tab.setAttribute("slot", "nav"));
return tabs.map((tab) => ({
el: tab,
width: tab.getBoundingClientRect().width
}));
}
/**
* Stores the measured width of the more dropdown trigger when available.
* @returns {void}
*/
measureMoreWidth() {
const wasHidden = this.moreDropdown.hidden;
const previousVisibility = this.moreDropdown.style.visibility;
if (wasHidden) {
this.moreDropdown.hidden = false;
this.moreDropdown.style.visibility = "hidden";
}
const width = this.moreDropdown.getBoundingClientRect().width || 0;
if (wasHidden) {
this.moreDropdown.hidden = true;
this.moreDropdown.style.visibility = previousVisibility;
}
if (width > 0) {
this._moreWidth = width;
}
}
/**
* Checks if the tabs within a navigation bar overflow the available space.
* Moves overflowing tabs into a dropdown menu and updates their state accordingly.
* @returns {void} This method does not return a value.
*/
checkOverflow() {
var _a, _b;
if (!this._initialized) return;
this._tabMetrics = this.measureTabMetrics();
this.measureMoreWidth();
const navWidth = this.nav.getBoundingClientRect().width;
const moreWidth = this._moreWidth || 48;
const totalWidth = this._tabMetrics.reduce((sum, { width }) => sum + width, 0);
if (totalWidth <= navWidth) {
this._tabMetrics.forEach(({ el }) => el.setAttribute("slot", "nav"));
this.toggleMoreVisibility();
this.dropdownActive((_a = this.getActiveTab()) == null ? void 0 : _a[0]);
return;
}
let used = 0;
let overflowStarted = false;
const availableWidth = Math.max(navWidth - moreWidth, 0);
for (const { el, width } of this._tabMetrics) {
used += width;
const shouldOverflow = used > availableWidth;
el.setAttribute("slot", shouldOverflow || overflowStarted ? "more" : "nav");
overflowStarted || (overflowStarted = shouldOverflow);
}
this.toggleMoreVisibility();
this.dropdownActive((_b = this.getActiveTab()) == null ? void 0 : _b[0]);
}
/**
* Toggles the "dropdown-active" class on the element based on its "active" status
* and the value of its "slot" attribute.
* @param {HTMLElement} el The HTML element to evaluate and apply the toggle logic.
* @returns {void} This method does not return any value.
*/
dropdownActive(el) {
if (!el) {
this.moreDropdown.classList.remove("dropdown-active");
return;
}
if (el.classList.contains("active")) {
if (el.getAttribute("slot") === "more")
this.moreDropdown.classList.add("dropdown-active");
else
this.moreDropdown.classList.remove("dropdown-active");
}
}
/**
* Syncs ARIA attributes on tabs and panels.
*/
syncAria() {
const tabs = this.getAllTabs();
const panels = this.getPanelAll();
const panelByName = new Map(panels.map((p) => [p.getAttribute("name"), p]));
this.id || `wje-tab-group-${this._instanceId}`;
this.setAriaState({
orientation: this.variant === "start" || this.variant === "end" ? "vertical" : "horizontal"
});
tabs.forEach((tab, index) => {
const tabName = tab.getAttribute(this.type) || tab.panel || tab.route || String(index);
const isActive = tab.classList.contains("active");
const isDisabled = tab.hasAttribute("disabled");
if (!tab.id) tab.id = `wje-tab-${tabName}`;
let controlsId = "";
if (this.type === "panel") {
const panel = panelByName.get(tabName);
if (panel) {
if (!panel.id) panel.id = `wje-tab-panel-${tabName}`;
controlsId = panel.id;
panel.setAriaState({
role: "tabpanel",
labelledBy: tab.id
});
}
}
tab.setAriaState({
role: "tab",
selected: isActive,
disabled: isDisabled,
controls: controlsId || ""
});
if (typeof tab.setRovingTabIndex === "function") {
tab.setRovingTabIndex(isActive ? 0 : -1);
}
});
}
disconnectedCallback() {
var _a, _b;
(_a = super.disconnectedCallback) == null ? void 0 : _a.call(this);
(_b = this._resizeObserver) == null ? void 0 : _b.disconnect();
}
};
__publicField(_TabGroup, "_instanceId", 0);
let TabGroup = _TabGroup;
TabGroup.define("wje-tab-group", TabGroup);
export {
TabGroup as default
};
//# sourceMappingURL=wje-tab-group.js.map