UNPKG

@umbraco-ui/uui-tabs

Version:

This package contains two elements, <uui-tab> and <uui-tab-group>

555 lines (527 loc) 19.3 kB
import { ActiveMixin, LabelMixin } from '@umbraco-ui/uui-base/lib/mixins'; import { defineElement } from '@umbraco-ui/uui-base/lib/registration'; import { LitElement, html, css } from 'lit'; import { property, query, queryAssignedElements } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { demandCustomElement } from '@umbraco-ui/uui-base/lib/utils'; import { repeat } from 'lit/directives/repeat.js'; import { UUIEvent } from '@umbraco-ui/uui-base/lib/events'; var __defProp$1 = Object.defineProperty; var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor; var __decorateClass$1 = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp$1(target, key, result); return result; }; let UUITabElement = class extends ActiveMixin(LabelMixin("", LitElement)) { constructor() { super(); this.disabled = false; this.orientation = "horizontal"; this.addEventListener("click", this.onHostClick); } onHostClick(e) { if (this.disabled) { e.preventDefault(); e.stopImmediatePropagation(); } } render() { return this.href ? html` <a id="button" href=${ifDefined(!this.disabled ? this.href : void 0)} target=${ifDefined(this.target || void 0)} rel=${ifDefined( this.rel || ifDefined( this.target === "_blank" ? "noopener noreferrer" : void 0 ) )} role="tab"> <slot name="icon"></slot> ${this.renderLabel()} <slot name="extra"></slot> </a> ` : html` <button type="button" id="button" ?disabled=${this.disabled} role="tab"> <slot name="icon"></slot> ${this.renderLabel()} <slot name="extra"></slot> </button> `; } }; UUITabElement.styles = [ css` :host { color: var(--uui-tab-text, var(--uui-color-interactive,#1b264f)); font-family: inherit; width: fit-content; } #button { position: relative; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; min-height: var(--uui-size-12,36px); min-width: 70px; padding: var(--uui-size-3,9px) var(--uui-tab-padding-horizontal, var(--uui-size-5,15px)); border: none; font-size: inherit; background: none; color: inherit; cursor: pointer; font-family: inherit; /* for anchor tag: */ text-decoration: none; line-height: normal; } :host([orientation='vertical']) #button { min-height: var(--uui-size-14,42px); padding: var(--uui-size-2,6px) var(--uui-tab-padding-horizontal, var(--uui-size-5,15px)); } :host(:not([disabled])) #button:hover { color: var(--uui-tab-text-hover, var(--uui-color-default-emphasis,#3544b1)); } :host(:not([disabled])) #button:active { box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.15), 0 1px 2px rgba(0, 0, 0, 0.05); } :host([active]) { color: var(--uui-tab-text-active, unset); } :host([disabled]) #button { color: var(--uui-color-disabled-contrast,#c4c4c4); cursor: default; } #button::before { content: ''; position: absolute; background-color: var(--uui-color-current,#f5c1bc); opacity: 0; } :host([active]) #button::before { opacity: 1; } /* HORIZONTAL */ :host([orientation='horizontal']) #button::before { left: auto; right: auto; border-radius: var(--uui-border-radius,3px) var(--uui-border-radius,3px) 0 0; height: 0px; width: calc(100% - 14px); bottom: 0; transition: opacity linear 120ms, height ease-in-out 120ms; } :host([active][orientation='horizontal']) #button::before { height: 4px; } /* VERTICAL */ :host([orientation='vertical']) #button::before { top: auto; bottom: auto; border-radius: 0 var(--uui-border-radius,3px) var(--uui-border-radius,3px) 0; height: calc(100% - 12px); width: 0px; left: 0; transition: opacity linear 120ms, width ease-in-out 120ms; } :host([active][orientation='vertical']) #button::before { width: 4px; } #button:hover::before { background-color: var(--uui-color-current-emphasis,rgb( 248, 214, 211 )); } :host([disabled]) #button::before { background-color: var(--uui-color-disabled-standalone,rgb( 226, 226, 226 )); } slot[name='icon']::slotted(*) { font-size: 20px; margin-bottom: var(--uui-size-2,6px); } slot.label { /* TODO: Find a better selector */ text-align: center; display: flex; width: 100%; flex-direction: column; } :host([orientation='vertical']) slot.label { text-align: left; } ` ]; __decorateClass$1([ property({ type: Boolean, reflect: true }) ], UUITabElement.prototype, "disabled", 2); __decorateClass$1([ property({ type: String }) ], UUITabElement.prototype, "href", 2); __decorateClass$1([ property({ type: String }) ], UUITabElement.prototype, "target", 2); __decorateClass$1([ property({ type: String }) ], UUITabElement.prototype, "rel", 2); __decorateClass$1([ property({ type: String, reflect: true }) ], UUITabElement.prototype, "orientation", 2); UUITabElement = __decorateClass$1([ defineElement("uui-tab") ], UUITabElement); var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __typeError = (msg) => { throw TypeError(msg); }; var __decorateClass = (decorators, target, key, kind) => { var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target; for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result; if (kind && result) __defProp(target, key, result); return result; }; var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg); var __privateGet = (obj, member, getter) => (__accessCheck(obj, member, "read from private field"), getter ? getter.call(obj) : member.get(obj)); var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var __privateSet = (obj, member, value, setter) => (__accessCheck(obj, member, "write to private field"), member.set(obj, value), value); var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); var _currentGap, _tabElements, _hiddenTabElements, _hiddenTabElementsMap, _visibilityBreakpoints, _resizeObserver, _tabResizeObservers, _breakPointCalculationInProgress, _UUITabGroupElement_instances, initialize_fn, onResize_fn, cleanupTabs_fn, onSlotChange_fn, _onTabClicked, calculateBreakPoints_fn, setTabArray_fn, updateCollapsibleTabs_fn, isElementTabLike_fn; let UUITabGroupElement = class extends LitElement { constructor() { super(...arguments); __privateAdd(this, _UUITabGroupElement_instances); /** Stores the current gap used in the breakpoints */ __privateAdd(this, _currentGap, 0); this.dropdownContentDirection = "vertical"; __privateAdd(this, _tabElements, []); __privateAdd(this, _hiddenTabElements, []); __privateAdd(this, _hiddenTabElementsMap, /* @__PURE__ */ new Map()); __privateAdd(this, _visibilityBreakpoints, []); __privateAdd(this, _resizeObserver, new ResizeObserver(__privateMethod(this, _UUITabGroupElement_instances, onResize_fn).bind(this))); __privateAdd(this, _tabResizeObservers, []); __privateAdd(this, _breakPointCalculationInProgress, false); __privateAdd(this, _onTabClicked, (e) => { const selectedElement = e.currentTarget; if (__privateMethod(this, _UUITabGroupElement_instances, isElementTabLike_fn).call(this, selectedElement)) { selectedElement.active = true; const linkedElement = __privateGet(this, _hiddenTabElementsMap).get(selectedElement); if (linkedElement) { linkedElement.active = true; } const filtered = [ ...__privateGet(this, _tabElements), ...__privateGet(this, _hiddenTabElements) ].filter((el) => el !== selectedElement && el !== linkedElement); filtered.forEach((el) => { if (__privateMethod(this, _UUITabGroupElement_instances, isElementTabLike_fn).call(this, el)) { el.active = false; } }); const hasActiveHidden = __privateGet(this, _hiddenTabElements).some( (el) => el.active && el !== linkedElement ); if (hasActiveHidden) { this._moreButtonElement.classList.add("active-inside"); } else { this._moreButtonElement.classList.remove("active-inside"); } } }); } connectedCallback() { super.connectedCallback(); __privateMethod(this, _UUITabGroupElement_instances, initialize_fn).call(this); } disconnectedCallback() { super.disconnectedCallback(); __privateGet(this, _resizeObserver).unobserve(this); __privateMethod(this, _UUITabGroupElement_instances, cleanupTabs_fn).call(this); } render() { return html` <div id="main"> <div id="grid" role="tablist"> <slot @slotchange=${__privateMethod(this, _UUITabGroupElement_instances, onSlotChange_fn)}></slot> </div> <uui-button popovertarget="popover-container" style="display: none" id="more-button" label="More" compact> <uui-symbol-more></uui-symbol-more> </uui-button> </div> <uui-popover-container id="popover-container" popover placement="bottom-end"> <div id="hidden-tabs-container" role="tablist"> ${repeat(__privateGet(this, _hiddenTabElements), (el) => html`${el}`)} </div> </uui-popover-container> `; } }; _currentGap = new WeakMap(); _tabElements = new WeakMap(); _hiddenTabElements = new WeakMap(); _hiddenTabElementsMap = new WeakMap(); _visibilityBreakpoints = new WeakMap(); _resizeObserver = new WeakMap(); _tabResizeObservers = new WeakMap(); _breakPointCalculationInProgress = new WeakMap(); _UUITabGroupElement_instances = new WeakSet(); initialize_fn = async function() { demandCustomElement(this, "uui-button"); demandCustomElement(this, "uui-popover-container"); demandCustomElement(this, "uui-symbol-more"); await this.updateComplete; __privateGet(this, _resizeObserver).observe(this._mainElement); }; onResize_fn = function(entries) { const gapCSSVar = Number.parseFloat( this.style.getPropertyValue("--uui-tab-group-gap") ); const newGap = Number.isNaN(gapCSSVar) ? 0 : gapCSSVar; if (newGap !== __privateGet(this, _currentGap)) { __privateMethod(this, _UUITabGroupElement_instances, calculateBreakPoints_fn).call(this); } else { __privateMethod(this, _UUITabGroupElement_instances, updateCollapsibleTabs_fn).call(this, entries[0].contentBoxSize[0].inlineSize); } }; cleanupTabs_fn = function() { __privateGet(this, _tabElements).forEach((el) => { el.removeEventListener("click", __privateGet(this, _onTabClicked)); __privateGet(this, _tabResizeObservers).forEach((observer) => observer.disconnect()); }); __privateGet(this, _tabResizeObservers).length = 0; __privateGet(this, _visibilityBreakpoints).length = 0; }; onSlotChange_fn = function() { __privateMethod(this, _UUITabGroupElement_instances, cleanupTabs_fn).call(this); __privateMethod(this, _UUITabGroupElement_instances, setTabArray_fn).call(this); __privateGet(this, _tabElements).forEach((el) => { el.addEventListener("click", __privateGet(this, _onTabClicked)); const observer = new ResizeObserver( __privateMethod(this, _UUITabGroupElement_instances, calculateBreakPoints_fn).bind(this) ); observer.observe(el); __privateGet(this, _tabResizeObservers).push(observer); }); }; _onTabClicked = new WeakMap(); calculateBreakPoints_fn = async function() { if (__privateGet(this, _breakPointCalculationInProgress)) return; __privateSet(this, _breakPointCalculationInProgress, true); requestAnimationFrame(() => { __privateSet(this, _breakPointCalculationInProgress, false); }); await this.updateComplete; const gapCSSVar = Number.parseFloat( this.style.getPropertyValue("--uui-tab-group-gap") ); const gap = Number.isNaN(gapCSSVar) ? 0 : gapCSSVar; __privateSet(this, _currentGap, gap); let childrenWidth = 0; for (let i = 0; i < __privateGet(this, _tabElements).length; i++) { __privateGet(this, _tabElements)[i].style.display = ""; childrenWidth += __privateGet(this, _tabElements)[i].offsetWidth; __privateGet(this, _visibilityBreakpoints)[i] = childrenWidth; childrenWidth += gap; } const tolerance = 2; this._mainElement.style.width = childrenWidth - gap + tolerance + "px"; __privateMethod(this, _UUITabGroupElement_instances, updateCollapsibleTabs_fn).call(this, this._mainElement.offsetWidth); }; setTabArray_fn = function() { __privateSet(this, _tabElements, this._slottedNodes ? this._slottedNodes : []); __privateMethod(this, _UUITabGroupElement_instances, calculateBreakPoints_fn).call(this); }; updateCollapsibleTabs_fn = function(containerWidth) { const moreButtonWidth = this._moreButtonElement.offsetWidth; const containerWithoutButtonWidth = containerWidth - (moreButtonWidth ? moreButtonWidth : 0); __privateGet(this, _hiddenTabElements).forEach((el) => { el.removeEventListener("click", __privateGet(this, _onTabClicked)); }); __privateSet(this, _hiddenTabElements, []); __privateGet(this, _hiddenTabElementsMap).clear(); let hasActiveTabInDropdown = false; const len = __privateGet(this, _visibilityBreakpoints).length; for (let i = 0; i < len; i++) { const breakpoint = __privateGet(this, _visibilityBreakpoints)[i]; const tab = __privateGet(this, _tabElements)[i]; if (breakpoint <= (i === len - 1 ? containerWidth : containerWithoutButtonWidth)) { tab.style.display = ""; } else { const proxyTab = tab.cloneNode(true); proxyTab.addEventListener("click", __privateGet(this, _onTabClicked)); proxyTab.classList.add("hidden-tab"); proxyTab.style.display = ""; proxyTab.orientation = this.dropdownContentDirection; __privateGet(this, _hiddenTabElementsMap).set(proxyTab, tab); __privateGet(this, _hiddenTabElementsMap).set(tab, proxyTab); __privateGet(this, _hiddenTabElements).push(proxyTab); tab.style.display = "none"; if (tab.active) { hasActiveTabInDropdown = true; } } } if (__privateGet(this, _hiddenTabElements).length === 0) { this._moreButtonElement.style.display = "none"; this._popoverContainerElement.hidePopover(); } else { this._moreButtonElement.style.display = ""; } if (hasActiveTabInDropdown) { this._moreButtonElement.classList.add("active-inside"); } else { this._moreButtonElement.classList.remove("active-inside"); } this.requestUpdate(); }; isElementTabLike_fn = function(el) { return typeof el === "object" && "active" in el && typeof el.active === "boolean"; }; UUITabGroupElement.styles = [ css` :host { min-width: 0; display: flex; height: 100%; } #main { display: flex; justify-content: space-between; overflow: hidden; } #grid { width: 1fr; display: flex; height: 100%; min-height: 48px; overflow: hidden; text-wrap: nowrap; color: var(--uui-tab-text); gap: var(--uui-tab-group-gap, 0); } #popover-container { --uui-tab-text: var(--uui-tab-group-dropdown-tab-text, unset); --uui-tab-text-hover: var( --uui-tab-group-dropdown-tab-text-hover, unset ); --uui-tab-text-active: var( --uui-tab-group-dropdown-tab-text-active, unset ); } ::slotted(*:not(:last-of-type)) { border-right: 1px solid var(--uui-tab-divider, none); } .hidden-tab { width: 100%; } #hidden-tabs-container { width: fit-content; display: flex; flex-direction: column; background-color: var( --uui-tab-group-dropdown-background, var(--uui-color-surface,#fff) ); border-radius: var(--uui-border-radius,3px); box-shadow: var(--uui-shadow-depth-3,0 10px 20px rgba(0,0,0,0.19) , 0 6px 6px rgba(0,0,0,0.23)); overflow: hidden; } :host([dropdown-direction='horizontal']) #hidden-tabs-container { flex-direction: row; } #more-button { position: relative; --uui-button-contrast: var(--uui-tab-text); --uui-button-contrast-hover: var(--uui-tab-text-hover); --uui-button-background-color: transparent; --uui-button-background-color-hover: transparent; } #more-button::before { content: ''; position: absolute; bottom: 0; width: 100%; background-color: var(--uui-color-current,#f5c1bc); height: 0px; border-radius: 3px 3px 0 0; opacity: 0; transition: opacity ease-in 120ms, height ease-in 120ms; } #more-button.active-inside::before { opacity: 1; height: 4px; transition: opacity 120ms, height ease-out 120ms; } ` ]; __decorateClass([ query("#more-button") ], UUITabGroupElement.prototype, "_moreButtonElement", 2); __decorateClass([ query("#popover-container") ], UUITabGroupElement.prototype, "_popoverContainerElement", 2); __decorateClass([ query("#main") ], UUITabGroupElement.prototype, "_mainElement", 2); __decorateClass([ queryAssignedElements({ flatten: true, selector: "uui-tab, [uui-tab], [role=tab]" }) ], UUITabGroupElement.prototype, "_slottedNodes", 2); __decorateClass([ property({ type: String, reflect: true, attribute: "dropdown-content-direction" }) ], UUITabGroupElement.prototype, "dropdownContentDirection", 2); UUITabGroupElement = __decorateClass([ defineElement("uui-tab-group") ], UUITabGroupElement); class UUITabEvent extends UUIEvent { } class UUITabGroupEvent extends UUIEvent { } export { UUITabElement, UUITabEvent, UUITabGroupElement, UUITabGroupEvent };