@umbraco-ui/uui-tabs
Version:
This package contains two elements, <uui-tab> and <uui-tab-group>
555 lines (527 loc) • 19.3 kB
JavaScript
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 };