@synergy-design-system/components
Version:
This package provides the base of the Synergy Design System as native web components. It uses [lit](https://www.lit.dev) and parts of [shoelace](https://shoelace.style/). Synergy officially supports the latest two versions of all major browsers (as define
519 lines (512 loc) • 18.5 kB
JavaScript
import {
tab_group_custom_styles_default
} from "./chunk.XOXVVU5C.js";
import {
tab_group_styles_default
} from "./chunk.PKYC7QF3.js";
import {
SynResizeObserver
} from "./chunk.ILXP2UV3.js";
import {
scrollIntoView
} from "./chunk.5732DMBC.js";
import {
SynIconButton
} from "./chunk.BANJ5DAQ.js";
import {
LocalizeController
} from "./chunk.OAQRCZOO.js";
import {
watch
} from "./chunk.BVZQ6QSY.js";
import {
component_styles_default
} from "./chunk.NLYVOJGK.js";
import {
SynergyElement
} from "./chunk.3AZFEB6D.js";
import {
__decorateClass,
__spreadValues
} from "./chunk.Z4XV3SMG.js";
// src/internal/scrollend-polyfill.ts
var debounce = (fn, delay) => {
let timerId = 0;
return function(...args) {
window.clearTimeout(timerId);
timerId = window.setTimeout(() => {
fn.call(this, ...args);
}, delay);
};
};
var decorate = (proto, method, decorateFn) => {
const superFn = proto[method];
proto[method] = function(...args) {
superFn.call(this, ...args);
decorateFn.call(this, superFn, ...args);
};
};
(() => {
if (typeof window === "undefined") {
return;
}
const isSupported = "onscrollend" in window;
if (!isSupported) {
const pointers = /* @__PURE__ */ new Set();
const scrollHandlers = /* @__PURE__ */ new WeakMap();
const handlePointerDown = (event) => {
for (const touch of event.changedTouches) {
pointers.add(touch.identifier);
}
};
const handlePointerUp = (event) => {
for (const touch of event.changedTouches) {
pointers.delete(touch.identifier);
}
};
document.addEventListener("touchstart", handlePointerDown, true);
document.addEventListener("touchend", handlePointerUp, true);
document.addEventListener("touchcancel", handlePointerUp, true);
decorate(EventTarget.prototype, "addEventListener", function(addEventListener, type) {
if (type !== "scrollend") return;
const handleScrollEnd = debounce(() => {
if (!pointers.size) {
this.dispatchEvent(new Event("scrollend"));
} else {
handleScrollEnd();
}
}, 100);
addEventListener.call(this, "scroll", handleScrollEnd, { passive: true });
scrollHandlers.set(this, handleScrollEnd);
});
decorate(EventTarget.prototype, "removeEventListener", function(removeEventListener, type) {
if (type !== "scrollend") return;
const scrollHandler = scrollHandlers.get(this);
if (scrollHandler) {
removeEventListener.call(this, "scroll", scrollHandler, { passive: true });
}
});
}
})();
// src/components/tab-group/tab-group.component.ts
import { classMap } from "lit/directives/class-map.js";
import { eventOptions, property, query, queryAssignedElements, state } from "lit/decorators.js";
import { html } from "lit";
var SynTabGroup = class extends SynergyElement {
constructor() {
super(...arguments);
this.focusableTabs = [];
this.localize = new LocalizeController(this);
this.hasScrollControls = false;
this.shouldHideScrollStartButton = false;
this.shouldHideScrollEndButton = false;
this.placement = "top";
this.activation = "auto";
this.noScrollControls = false;
this.contained = false;
this.sharp = false;
this.fixedScrollControls = false;
/**
* The reality of the browser means that we can't expect the scroll position to be exactly what we want it to be, so
* we add one pixel of wiggle room to our calculations.
*/
this.scrollOffset = 1;
}
connectedCallback() {
const whenAllDefined = Promise.all([
customElements.whenDefined("syn-tab"),
customElements.whenDefined("syn-tab-panel")
]);
super.connectedCallback();
this.resizeObserver = new ResizeObserver(() => {
this.repositionIndicator();
this.updateScrollControls();
});
this.mutationObserver = new MutationObserver((mutations) => {
const instanceMutations = mutations.filter(({ target }) => {
if (target === this) return true;
if (target.closest("syn-tab-group") !== this) return false;
const tagName = target.tagName.toLowerCase();
return tagName === "syn-tab" || tagName === "syn-tab-panel";
});
if (instanceMutations.length === 0) {
return;
}
if (instanceMutations.some((m) => !["aria-labelledby", "aria-controls"].includes(m.attributeName))) {
setTimeout(() => this.setAriaLabels());
}
if (instanceMutations.some((m) => m.attributeName === "disabled")) {
this.syncTabsAndPanels();
} else if (instanceMutations.some((m) => m.attributeName === "active")) {
const tabs = instanceMutations.filter((m) => m.attributeName === "active" && m.target.tagName.toLowerCase() === "syn-tab").map((m) => m.target);
const newActiveTab = tabs.find((tab) => tab.active);
if (newActiveTab) {
this.setActiveTab(newActiveTab);
}
}
});
this.updateComplete.then(() => {
this.syncTabsAndPanels();
this.mutationObserver.observe(this, {
attributes: true,
attributeFilter: ["active", "disabled", "name", "panel"],
childList: true,
subtree: true
});
this.resizeObserver.observe(this.nav);
whenAllDefined.then(() => {
const intersectionObserver = new IntersectionObserver((entries, observer) => {
var _a;
if (entries[0].intersectionRatio > 0) {
this.setAriaLabels();
this.setActiveTab((_a = this.getActiveTab()) != null ? _a : this.tabs[0], { emitEvents: false });
observer.unobserve(entries[0].target);
}
});
intersectionObserver.observe(this.tabGroup);
});
});
}
disconnectedCallback() {
var _a, _b;
super.disconnectedCallback();
(_a = this.mutationObserver) == null ? void 0 : _a.disconnect();
if (this.nav) {
(_b = this.resizeObserver) == null ? void 0 : _b.unobserve(this.nav);
}
}
getActiveTab() {
return this.tabs.find((el) => el.active);
}
handleClick(event) {
const target = event.target;
const tab = target.closest("syn-tab");
const tabGroup = tab == null ? void 0 : tab.closest("syn-tab-group");
if (tabGroup !== this) {
return;
}
if (tab !== null) {
this.setActiveTab(tab, { scrollBehavior: "smooth" });
}
}
handleKeyDown(event) {
const target = event.target;
const tab = target.closest("syn-tab");
const tabGroup = tab == null ? void 0 : tab.closest("syn-tab-group");
if (tabGroup !== this) {
return;
}
if (["Enter", " "].includes(event.key)) {
if (tab !== null) {
this.setActiveTab(tab, { scrollBehavior: "smooth" });
event.preventDefault();
}
}
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End"].includes(event.key)) {
const activeEl = this.tabs.find((t) => t.matches(":focus"));
const isRtl = this.localize.dir() === "rtl";
let nextTab = null;
if ((activeEl == null ? void 0 : activeEl.tagName.toLowerCase()) === "syn-tab") {
if (event.key === "Home") {
nextTab = this.focusableTabs[0];
} else if (event.key === "End") {
nextTab = this.focusableTabs[this.focusableTabs.length - 1];
} else if (["top"].includes(this.placement) && event.key === (isRtl ? "ArrowRight" : "ArrowLeft") || ["start", "end"].includes(this.placement) && event.key === "ArrowUp") {
const currentIndex = this.tabs.findIndex((el) => el === activeEl);
nextTab = this.findNextFocusableTab(currentIndex, "backward");
} else if (["top"].includes(this.placement) && event.key === (isRtl ? "ArrowLeft" : "ArrowRight") || ["start", "end"].includes(this.placement) && event.key === "ArrowDown") {
const currentIndex = this.tabs.findIndex((el) => el === activeEl);
nextTab = this.findNextFocusableTab(currentIndex, "forward");
}
if (!nextTab) {
return;
}
nextTab.tabIndex = 0;
nextTab.focus({ preventScroll: true });
if (this.activation === "auto") {
this.setActiveTab(nextTab, { scrollBehavior: "smooth" });
} else {
this.tabs.forEach((tabEl) => {
tabEl.tabIndex = tabEl === nextTab ? 0 : -1;
});
}
if (["top"].includes(this.placement)) {
scrollIntoView(nextTab, this.nav, "horizontal");
}
event.preventDefault();
}
}
}
handleScrollToStart() {
this.nav.scroll({
left: this.localize.dir() === "rtl" ? this.nav.scrollLeft + this.nav.clientWidth : this.nav.scrollLeft - this.nav.clientWidth,
behavior: "smooth"
});
}
handleScrollToEnd() {
this.nav.scroll({
left: this.localize.dir() === "rtl" ? this.nav.scrollLeft - this.nav.clientWidth : this.nav.scrollLeft + this.nav.clientWidth,
behavior: "smooth"
});
}
setActiveTab(tab, options) {
options = __spreadValues({
emitEvents: true,
scrollBehavior: "auto"
}, options);
if (tab !== this.activeTab && !tab.disabled) {
const previousTab = this.activeTab;
this.activeTab = tab;
this.tabs.forEach((el) => {
el.active = el === this.activeTab;
el.tabIndex = el === this.activeTab ? 0 : -1;
});
this.panels.forEach((el) => {
var _a;
return el.active = el.name === ((_a = this.activeTab) == null ? void 0 : _a.panel);
});
this.syncIndicator();
if (["top"].includes(this.placement)) {
scrollIntoView(this.activeTab, this.nav, "horizontal", options.scrollBehavior);
}
if (options.emitEvents) {
if (previousTab) {
this.emit("syn-tab-hide", { detail: { name: previousTab.panel } });
}
this.emit("syn-tab-show", { detail: { name: this.activeTab.panel } });
}
}
}
setAriaLabels() {
this.tabs.forEach((tab) => {
const panel = this.panels.find((el) => el.name === tab.panel);
if (panel) {
tab.setAttribute("aria-controls", panel.getAttribute("id"));
panel.setAttribute("aria-labelledby", tab.getAttribute("id"));
}
});
}
repositionIndicator() {
const currentTab = this.getActiveTab();
if (!currentTab) {
return;
}
const width = currentTab.clientWidth;
const height = currentTab.clientHeight;
const isRtl = this.localize.dir() === "rtl";
const precedingTabs = this.tabs.slice(0, this.tabs.indexOf(currentTab));
const offset = precedingTabs.reduce(
(previous, current) => ({
left: previous.left + current.clientWidth,
top: previous.top + current.clientHeight
}),
{ left: 0, top: 0 }
);
switch (this.placement) {
case "top":
this.indicator.style.width = `calc(${width}px - ${this.contained || this.sharp ? "2 * var(--syn-spacing-large)" : "0px"})`;
this.indicator.style.height = "auto";
this.indicator.style.translate = `calc(${isRtl ? "-" : ""}1 * (${offset.left}px + ${this.contained || this.sharp ? "var(--syn-spacing-large)" : "0px"}))`;
break;
case "start":
case "end":
this.indicator.style.width = "auto";
this.indicator.style.height = `calc(${height}px - ${this.contained || this.sharp ? "2 * var(--syn-spacing-small)" : "0px"})`;
this.indicator.style.translate = `0 calc(${offset.top}px + ${this.contained || this.sharp ? "var(--syn-spacing-small)" : "0px"})`;
break;
}
}
// This stores tabs and panels so we can refer to a cache instead of calling querySelectorAll() multiple times.
syncTabsAndPanels() {
this.focusableTabs = this.tabs.filter((el) => !el.disabled);
this.syncIndicator();
this.updateComplete.then(() => this.updateScrollControls());
}
findNextFocusableTab(currentIndex, direction) {
let nextTab = null;
const iterator = direction === "forward" ? 1 : -1;
let nextIndex = currentIndex + iterator;
while (currentIndex < this.tabs.length) {
nextTab = this.tabs[nextIndex] || null;
if (nextTab === null) {
if (direction === "forward") {
nextTab = this.focusableTabs[0];
} else {
nextTab = this.focusableTabs[this.focusableTabs.length - 1];
}
break;
}
if (!nextTab.disabled) {
break;
}
nextIndex += iterator;
}
return nextTab;
}
updateScrollButtons() {
if (this.hasScrollControls && !this.fixedScrollControls) {
this.shouldHideScrollStartButton = this.scrollFromStart() <= this.scrollOffset;
this.shouldHideScrollEndButton = this.isScrolledToEnd();
}
}
isScrolledToEnd() {
return this.scrollFromStart() + this.nav.clientWidth >= this.nav.scrollWidth - this.scrollOffset;
}
scrollFromStart() {
return this.localize.dir() === "rtl" ? -this.nav.scrollLeft : this.nav.scrollLeft;
}
updateScrollControls() {
if (this.noScrollControls) {
this.hasScrollControls = false;
} else {
this.hasScrollControls = ["top"].includes(this.placement) && this.nav.scrollWidth > this.nav.clientWidth + 1;
}
this.updateScrollButtons();
}
syncIndicator() {
const tab = this.getActiveTab();
if (tab) {
this.indicator.style.display = "block";
this.repositionIndicator();
} else {
this.indicator.style.display = "none";
}
}
/** Shows the specified tab panel. */
show(panel) {
const tab = this.tabs.find((el) => el.panel === panel);
if (tab) {
this.setActiveTab(tab, { scrollBehavior: "smooth" });
}
}
preventFocus(e) {
e.preventDefault();
}
render() {
return html`
<div
part="base"
class=${classMap({
"tab-group": true,
"tab-group--top": this.placement === "top",
"tab-group--start": this.placement === "start",
"tab-group--end": this.placement === "end",
"tab-group--rtl": this.localize.dir() === "rtl",
"tab-group--has-scroll-controls": this.hasScrollControls,
"tab-group--contained": this.contained,
"tab-group--sharp": this.sharp
})}
=${this.handleClick}
=${this.handleKeyDown}
>
<div class="tab-group__nav-container" part="nav">
${this.hasScrollControls ? html`
<syn-icon-button
part="scroll-button scroll-button--start"
exportparts="base:scroll-button__base"
class=${classMap({
"tab-group__scroll-button": true,
"tab-group__scroll-button--start": true,
"tab-group__scroll-button--start--hidden": this.shouldHideScrollStartButton
})}
name="chevron-right"
library="system"
tabindex="-1"
aria-hidden="true"
label=${this.localize.term("scrollToStart")}
=${this.preventFocus}
=${this.handleScrollToStart}
></syn-icon-button>
` : ""}
<div class="tab-group__nav" =${this.updateScrollButtons}>
<div part="tabs" class="tab-group__tabs" role="tablist">
<div part="active-tab-indicator" class="tab-group__indicator"></div>
<syn-resize-observer -resize=${this.syncIndicator}>
<slot name="nav" =${this.syncTabsAndPanels}></slot>
</syn-resize-observer>
</div>
</div>
${this.hasScrollControls ? html`
<syn-icon-button
part="scroll-button scroll-button--end"
exportparts="base:scroll-button__base"
class=${classMap({
"tab-group__scroll-button": true,
"tab-group__scroll-button--end": true,
"tab-group__scroll-button--end--hidden": this.shouldHideScrollEndButton
})}
name="chevron-right"
library="system"
tabindex="-1"
aria-hidden="true"
label=${this.localize.term("scrollToEnd")}
=${this.preventFocus}
=${this.handleScrollToEnd}
></syn-icon-button>
` : ""}
</div>
<slot part="body" class="tab-group__body" =${this.syncTabsAndPanels}></slot>
</div>
`;
}
};
SynTabGroup.styles = [component_styles_default, tab_group_styles_default, tab_group_custom_styles_default];
SynTabGroup.dependencies = { "syn-icon-button": SynIconButton, "syn-resize-observer": SynResizeObserver };
__decorateClass([
queryAssignedElements({ slot: "nav", selector: "syn-tab" })
], SynTabGroup.prototype, "tabs", 2);
__decorateClass([
queryAssignedElements({ selector: "syn-tab-panel" })
], SynTabGroup.prototype, "panels", 2);
__decorateClass([
query(".tab-group")
], SynTabGroup.prototype, "tabGroup", 2);
__decorateClass([
query(".tab-group__body")
], SynTabGroup.prototype, "body", 2);
__decorateClass([
query(".tab-group__nav")
], SynTabGroup.prototype, "nav", 2);
__decorateClass([
query(".tab-group__indicator")
], SynTabGroup.prototype, "indicator", 2);
__decorateClass([
state()
], SynTabGroup.prototype, "hasScrollControls", 2);
__decorateClass([
state()
], SynTabGroup.prototype, "shouldHideScrollStartButton", 2);
__decorateClass([
state()
], SynTabGroup.prototype, "shouldHideScrollEndButton", 2);
__decorateClass([
property()
], SynTabGroup.prototype, "placement", 2);
__decorateClass([
property()
], SynTabGroup.prototype, "activation", 2);
__decorateClass([
property({ attribute: "no-scroll-controls", type: Boolean })
], SynTabGroup.prototype, "noScrollControls", 2);
__decorateClass([
property({ type: Boolean })
], SynTabGroup.prototype, "contained", 2);
__decorateClass([
property({ type: Boolean })
], SynTabGroup.prototype, "sharp", 2);
__decorateClass([
property({ attribute: "fixed-scroll-controls", type: Boolean })
], SynTabGroup.prototype, "fixedScrollControls", 2);
__decorateClass([
eventOptions({ passive: true })
], SynTabGroup.prototype, "updateScrollButtons", 1);
__decorateClass([
watch("noScrollControls", { waitUntilFirstUpdate: true })
], SynTabGroup.prototype, "updateScrollControls", 1);
__decorateClass([
watch("placement", { waitUntilFirstUpdate: true })
], SynTabGroup.prototype, "syncIndicator", 1);
export {
SynTabGroup
};
//# sourceMappingURL=chunk.CBVQAM42.js.map