UNPKG

@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
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 })} @click=${this.handleClick} @keydown=${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")} @mousedown=${this.preventFocus} @click=${this.handleScrollToStart} ></syn-icon-button> ` : ""} <div class="tab-group__nav" @scrollend=${this.updateScrollButtons}> <div part="tabs" class="tab-group__tabs" role="tablist"> <div part="active-tab-indicator" class="tab-group__indicator"></div> <syn-resize-observer @syn-resize=${this.syncIndicator}> <slot name="nav" @slotchange=${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")} @mousedown=${this.preventFocus} @click=${this.handleScrollToEnd} ></syn-icon-button> ` : ""} </div> <slot part="body" class="tab-group__body" @slotchange=${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