2025-12-08 12:04:01 +00:00
|
|
|
import * as interfaces from '../../interfaces/index.js';
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
DeesElement,
|
|
|
|
|
type TemplateResult,
|
|
|
|
|
property,
|
2025-12-30 10:27:34 +00:00
|
|
|
state,
|
2025-06-17 08:41:36 +00:00
|
|
|
customElement,
|
|
|
|
|
html,
|
|
|
|
|
css,
|
|
|
|
|
cssManager,
|
|
|
|
|
} from '@design.estate/dees-element';
|
|
|
|
|
|
|
|
|
|
import * as domtools from '@design.estate/dees-domtools';
|
2025-12-29 01:20:24 +00:00
|
|
|
import { demoFunc } from './dees-appui-tabs.demo.js';
|
|
|
|
|
import { themeDefaultStyles } from '../../00theme.js';
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@customElement('dees-appui-tabs')
|
|
|
|
|
export class DeesAppuiTabs extends DeesElement {
|
2025-12-29 01:20:24 +00:00
|
|
|
public static demo = demoFunc;
|
2026-01-04 17:09:18 +00:00
|
|
|
public static demoGroup = 'App UI';
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
// INSTANCE
|
|
|
|
|
@property({
|
|
|
|
|
type: Array,
|
|
|
|
|
})
|
2025-12-29 01:20:24 +00:00
|
|
|
accessor tabs: interfaces.IMenuItem[] = [];
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: Object })
|
2025-12-29 01:20:24 +00:00
|
|
|
accessor selectedTab: interfaces.IMenuItem | null = null;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: Boolean })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor showTabIndicator: boolean = true;
|
2025-06-17 08:41:36 +00:00
|
|
|
|
|
|
|
|
@property({ type: String })
|
2025-11-17 13:27:11 +00:00
|
|
|
accessor tabStyle: 'horizontal' | 'vertical' = 'horizontal';
|
2025-06-17 08:41:36 +00:00
|
|
|
|
2025-12-29 23:33:38 +00:00
|
|
|
@property({ type: Boolean })
|
|
|
|
|
accessor autoHide: boolean = false;
|
|
|
|
|
|
|
|
|
|
@property({ type: Number })
|
|
|
|
|
accessor autoHideThreshold: number = 0;
|
|
|
|
|
|
2025-12-30 10:27:34 +00:00
|
|
|
// Scroll state for fade indicators
|
|
|
|
|
@state()
|
|
|
|
|
private accessor canScrollLeft: boolean = false;
|
|
|
|
|
|
|
|
|
|
@state()
|
|
|
|
|
private accessor canScrollRight: boolean = false;
|
|
|
|
|
|
|
|
|
|
private resizeObserver: ResizeObserver | null = null;
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
public static styles = [
|
2025-12-29 01:20:24 +00:00
|
|
|
themeDefaultStyles,
|
2025-06-17 08:41:36 +00:00
|
|
|
cssManager.defaultStyles,
|
|
|
|
|
css`
|
2025-12-29 01:20:24 +00:00
|
|
|
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
2025-06-17 08:41:36 +00:00
|
|
|
:host {
|
|
|
|
|
display: block;
|
|
|
|
|
position: relative;
|
|
|
|
|
width: 100%;
|
2025-12-30 10:27:34 +00:00
|
|
|
min-width: 0;
|
|
|
|
|
overflow: hidden;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-wrapper {
|
|
|
|
|
position: relative;
|
2025-12-30 10:27:34 +00:00
|
|
|
min-width: 0;
|
2025-06-27 22:47:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-wrapper.horizontal-wrapper {
|
2025-12-08 14:50:53 +00:00
|
|
|
height: 48px;
|
2025-06-27 22:47:24 +00:00
|
|
|
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
2025-12-08 14:50:53 +00:00
|
|
|
box-sizing: border-box;
|
2025-12-30 10:27:34 +00:00
|
|
|
overflow: hidden;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Scroll fade indicators */
|
|
|
|
|
.scroll-fade {
|
|
|
|
|
position: absolute;
|
|
|
|
|
top: 0;
|
|
|
|
|
bottom: 1px;
|
|
|
|
|
width: 48px;
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.2s ease;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scroll-fade-left {
|
|
|
|
|
left: 0;
|
|
|
|
|
background: linear-gradient(to right,
|
|
|
|
|
${cssManager.bdTheme('#ffffff', '#161616')} 0%,
|
|
|
|
|
${cssManager.bdTheme('rgba(255,255,255,0)', 'rgba(22,22,22,0)')} 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scroll-fade-right {
|
|
|
|
|
right: 0;
|
|
|
|
|
background: linear-gradient(to left,
|
|
|
|
|
${cssManager.bdTheme('#ffffff', '#161616')} 0%,
|
|
|
|
|
${cssManager.bdTheme('rgba(255,255,255,0)', 'rgba(22,22,22,0)')} 100%);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.scroll-fade.visible {
|
|
|
|
|
opacity: 1;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabsContainer {
|
|
|
|
|
position: relative;
|
|
|
|
|
user-select: none;
|
2025-12-30 10:27:34 +00:00
|
|
|
min-width: 0;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabsContainer.horizontal {
|
2025-06-27 22:47:24 +00:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
2025-06-17 08:41:36 +00:00
|
|
|
font-size: 14px;
|
2025-06-27 22:47:24 +00:00
|
|
|
overflow-x: auto;
|
2025-12-30 10:27:34 +00:00
|
|
|
overflow-y: hidden;
|
2026-01-03 02:50:37 +00:00
|
|
|
overscroll-behavior: contain;
|
2025-12-30 10:27:34 +00:00
|
|
|
scrollbar-width: thin;
|
|
|
|
|
scrollbar-color: transparent transparent;
|
2025-12-08 14:50:53 +00:00
|
|
|
height: 100%;
|
2025-06-27 22:47:24 +00:00
|
|
|
padding: 0 16px;
|
|
|
|
|
gap: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 10:27:34 +00:00
|
|
|
/* Show scrollbar on hover */
|
|
|
|
|
.tabs-wrapper:hover .tabsContainer.horizontal {
|
|
|
|
|
scrollbar-color: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')} transparent;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:47:24 +00:00
|
|
|
.tabsContainer.horizontal::-webkit-scrollbar {
|
2025-12-30 10:27:34 +00:00
|
|
|
height: 4px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabsContainer.horizontal::-webkit-scrollbar-track {
|
|
|
|
|
background: transparent;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabsContainer.horizontal::-webkit-scrollbar-thumb {
|
|
|
|
|
background: transparent;
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
transition: background 0.2s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0,0,0,0.2)', 'rgba(255,255,255,0.2)')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-wrapper:hover .tabsContainer.horizontal::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0,0,0,0.35)', 'rgba(255,255,255,0.35)')};
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabsContainer.vertical {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction: column;
|
2025-06-27 22:47:24 +00:00
|
|
|
padding: 8px;
|
2025-06-17 08:41:36 +00:00
|
|
|
font-size: 14px;
|
2025-06-27 22:47:24 +00:00
|
|
|
gap: 2px;
|
|
|
|
|
position: relative;
|
|
|
|
|
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
|
|
|
|
border-radius: 8px;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab {
|
2025-06-27 22:47:24 +00:00
|
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
2025-06-17 08:41:36 +00:00
|
|
|
white-space: nowrap;
|
2025-06-27 22:47:24 +00:00
|
|
|
cursor: pointer;
|
|
|
|
|
transition: color 0.15s ease;
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
position: relative;
|
|
|
|
|
z-index: 2;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.horizontal .tab {
|
2025-06-27 22:47:24 +00:00
|
|
|
padding: 0 16px;
|
|
|
|
|
height: 100%;
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
|
|
|
|
position: relative;
|
|
|
|
|
border-radius: 6px 6px 0 0;
|
|
|
|
|
transition: background-color 0.15s ease;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.horizontal .tab:not(:last-child)::after {
|
|
|
|
|
content: '';
|
|
|
|
|
position: absolute;
|
|
|
|
|
right: -2px;
|
|
|
|
|
top: 50%;
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
height: 20px;
|
|
|
|
|
width: 1px;
|
|
|
|
|
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.horizontal .tab .tab-content {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vertical .tab {
|
2025-06-27 22:47:24 +00:00
|
|
|
padding: 10px 16px;
|
|
|
|
|
border-radius: 6px;
|
2025-06-17 08:41:36 +00:00
|
|
|
width: 100%;
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
2025-06-27 22:47:24 +00:00
|
|
|
transition: all 0.15s ease;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab:hover {
|
2025-06-27 22:47:24 +00:00
|
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.horizontal .tab:hover {
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.03)')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.horizontal .tab:hover::after,
|
|
|
|
|
.horizontal .tab:hover + .tab::after {
|
|
|
|
|
opacity: 0;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vertical .tab:hover {
|
2025-06-27 22:47:24 +00:00
|
|
|
background: ${cssManager.bdTheme('rgba(244, 244, 245, 0.5)', 'rgba(39, 39, 42, 0.5)')};
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:47:24 +00:00
|
|
|
.horizontal .tab.selectedTab {
|
|
|
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.horizontal .tab.selectedTab::after,
|
|
|
|
|
.horizontal .tab.selectedTab + .tab::after {
|
|
|
|
|
opacity: 0;
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vertical .tab.selectedTab {
|
2025-06-27 22:47:24 +00:00
|
|
|
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab dees-icon {
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:47:24 +00:00
|
|
|
.tabIndicator {
|
2025-06-17 08:41:36 +00:00
|
|
|
position: absolute;
|
2025-06-27 22:47:24 +00:00
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
opacity: 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabIndicator.no-transition {
|
|
|
|
|
transition: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tabs-wrapper .tabIndicator {
|
|
|
|
|
height: 3px;
|
|
|
|
|
bottom: 0;
|
|
|
|
|
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
|
|
|
|
border-radius: 3px 3px 0 0;
|
|
|
|
|
z-index: 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vertical-wrapper {
|
|
|
|
|
position: relative;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.vertical-wrapper .tabIndicator {
|
|
|
|
|
left: 8px;
|
|
|
|
|
right: 8px;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
|
|
|
|
|
z-index: 1;
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
2025-12-29 23:33:38 +00:00
|
|
|
|
|
|
|
|
/* Close button */
|
|
|
|
|
.tab-close {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
width: 16px;
|
|
|
|
|
height: 16px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
transition: opacity 0.15s, background 0.15s;
|
|
|
|
|
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab:hover .tab-close {
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab-close:hover {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
background: ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')};
|
|
|
|
|
color: ${cssManager.bdTheme('#ef4444', '#f87171')};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab.selectedTab .tab-close {
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab.selectedTab:hover .tab-close {
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.tab.selectedTab .tab-close:hover {
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
2025-06-17 08:41:36 +00:00
|
|
|
`,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
public render(): TemplateResult {
|
2025-12-29 23:33:38 +00:00
|
|
|
// Auto-hide when enabled and tab count is at or below threshold
|
|
|
|
|
if (this.autoHide && this.tabs.length <= this.autoHideThreshold) {
|
|
|
|
|
return html``;
|
|
|
|
|
}
|
2025-06-17 08:41:36 +00:00
|
|
|
return html`
|
2025-06-27 22:47:24 +00:00
|
|
|
${this.renderTabsWrapper()}
|
2025-06-17 08:41:36 +00:00
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:47:24 +00:00
|
|
|
private renderTabsWrapper(): TemplateResult {
|
|
|
|
|
const isHorizontal = this.tabStyle === 'horizontal';
|
|
|
|
|
const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper';
|
|
|
|
|
const containerClass = `tabsContainer ${this.tabStyle}`;
|
|
|
|
|
|
2025-12-30 10:27:34 +00:00
|
|
|
if (isHorizontal) {
|
|
|
|
|
return html`
|
|
|
|
|
<div class="${wrapperClass}">
|
|
|
|
|
<div class="scroll-fade scroll-fade-left ${this.canScrollLeft ? 'visible' : ''}"></div>
|
|
|
|
|
<div class="${containerClass}" @scroll=${this.handleScroll}>
|
|
|
|
|
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
|
|
|
|
|
</div>
|
|
|
|
|
<div class="scroll-fade scroll-fade-right ${this.canScrollRight ? 'visible' : ''}"></div>
|
|
|
|
|
${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:47:24 +00:00
|
|
|
return html`
|
|
|
|
|
<div class="${wrapperClass}">
|
|
|
|
|
<div class="${containerClass}">
|
|
|
|
|
${this.tabs.map(tab => this.renderTab(tab, isHorizontal))}
|
|
|
|
|
</div>
|
|
|
|
|
${this.showTabIndicator ? html`<div class="tabIndicator"></div>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 01:20:24 +00:00
|
|
|
private renderTab(tab: interfaces.IMenuItem, isHorizontal: boolean): TemplateResult {
|
2025-06-27 22:47:24 +00:00
|
|
|
const isSelected = tab === this.selectedTab;
|
|
|
|
|
const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
|
2025-12-29 23:33:38 +00:00
|
|
|
|
|
|
|
|
const closeButton = tab.closeable ? html`
|
|
|
|
|
<span class="tab-close" @click="${(e: Event) => this.closeTab(e, tab)}">
|
|
|
|
|
<dees-icon .icon=${'lucide:x'} style="font-size: 12px;"></dees-icon>
|
|
|
|
|
</span>
|
|
|
|
|
` : '';
|
|
|
|
|
|
2025-06-27 22:47:24 +00:00
|
|
|
const content = isHorizontal ? html`
|
|
|
|
|
<span class="tab-content">
|
|
|
|
|
${this.renderTabIcon(tab)}
|
|
|
|
|
${tab.key}
|
|
|
|
|
</span>
|
2025-12-29 23:33:38 +00:00
|
|
|
${closeButton}
|
2025-06-27 22:47:24 +00:00
|
|
|
` : html`
|
|
|
|
|
${this.renderTabIcon(tab)}
|
|
|
|
|
${tab.key}
|
2025-12-29 23:33:38 +00:00
|
|
|
${closeButton}
|
2025-06-27 22:47:24 +00:00
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<div
|
|
|
|
|
class="${classes}"
|
|
|
|
|
@click="${() => this.selectTab(tab)}"
|
|
|
|
|
>
|
|
|
|
|
${content}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 01:20:24 +00:00
|
|
|
private renderTabIcon(tab: interfaces.IMenuItem): TemplateResult | '' {
|
2025-06-27 22:47:24 +00:00
|
|
|
return tab.iconName ? html`<dees-icon .icon=${tab.iconName}></dees-icon>` : '';
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 01:20:24 +00:00
|
|
|
private selectTab(tabArg: interfaces.IMenuItem) {
|
2025-06-17 08:41:36 +00:00
|
|
|
this.selectedTab = tabArg;
|
|
|
|
|
tabArg.action();
|
2025-12-29 23:33:38 +00:00
|
|
|
|
2025-12-30 10:27:34 +00:00
|
|
|
// Scroll selected tab into view
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.scrollTabIntoView(tabArg);
|
|
|
|
|
});
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
// Emit tab-select event
|
|
|
|
|
this.dispatchEvent(new CustomEvent('tab-select', {
|
|
|
|
|
detail: { tab: tabArg },
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 23:33:38 +00:00
|
|
|
private closeTab(e: Event, tab: interfaces.IMenuItem) {
|
|
|
|
|
e.stopPropagation(); // Don't select tab when closing
|
|
|
|
|
|
|
|
|
|
// Call the tab's onClose callback if defined
|
|
|
|
|
if (tab.onClose) {
|
|
|
|
|
tab.onClose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Also emit event for parent components
|
|
|
|
|
this.dispatchEvent(new CustomEvent('tab-close', {
|
|
|
|
|
detail: { tab },
|
|
|
|
|
bubbles: true,
|
|
|
|
|
composed: true
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
firstUpdated() {
|
|
|
|
|
if (this.tabs && this.tabs.length > 0) {
|
|
|
|
|
this.selectTab(this.tabs[0]);
|
|
|
|
|
}
|
2025-12-30 10:27:34 +00:00
|
|
|
|
|
|
|
|
// Set up ResizeObserver for scroll state updates
|
|
|
|
|
this.setupResizeObserver();
|
|
|
|
|
|
|
|
|
|
// Initial scroll state check
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.updateScrollState();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async disconnectedCallback() {
|
|
|
|
|
await super.disconnectedCallback();
|
|
|
|
|
if (this.resizeObserver) {
|
|
|
|
|
this.resizeObserver.disconnect();
|
|
|
|
|
this.resizeObserver = null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private setupResizeObserver() {
|
|
|
|
|
if (this.tabStyle !== 'horizontal') return;
|
|
|
|
|
|
|
|
|
|
this.resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
this.updateScrollState();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const container = this.shadowRoot?.querySelector('.tabsContainer.horizontal');
|
|
|
|
|
if (container) {
|
|
|
|
|
this.resizeObserver.observe(container);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private handleScroll = () => {
|
|
|
|
|
this.updateScrollState();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private updateScrollState() {
|
|
|
|
|
const container = this.shadowRoot?.querySelector('.tabsContainer.horizontal') as HTMLElement;
|
|
|
|
|
if (!container) return;
|
|
|
|
|
|
|
|
|
|
const scrollLeft = container.scrollLeft;
|
|
|
|
|
const scrollWidth = container.scrollWidth;
|
|
|
|
|
const clientWidth = container.clientWidth;
|
|
|
|
|
|
|
|
|
|
// Small threshold to account for rounding
|
|
|
|
|
const threshold = 2;
|
|
|
|
|
|
|
|
|
|
this.canScrollLeft = scrollLeft > threshold;
|
|
|
|
|
this.canScrollRight = scrollLeft < scrollWidth - clientWidth - threshold;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private scrollTabIntoView(tab: interfaces.IMenuItem) {
|
|
|
|
|
if (this.tabStyle !== 'horizontal') return;
|
|
|
|
|
|
|
|
|
|
const tabIndex = this.tabs.indexOf(tab);
|
|
|
|
|
if (tabIndex === -1) return;
|
|
|
|
|
|
|
|
|
|
const container = this.shadowRoot?.querySelector('.tabsContainer.horizontal') as HTMLElement;
|
|
|
|
|
const tabElement = container?.querySelector(`.tab:nth-child(${tabIndex + 1})`) as HTMLElement;
|
|
|
|
|
|
|
|
|
|
if (tabElement && container) {
|
|
|
|
|
const containerRect = container.getBoundingClientRect();
|
|
|
|
|
const tabRect = tabElement.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
// Check if tab is fully visible
|
|
|
|
|
const isFullyVisible =
|
|
|
|
|
tabRect.left >= containerRect.left &&
|
|
|
|
|
tabRect.right <= containerRect.right;
|
|
|
|
|
|
|
|
|
|
if (!isFullyVisible) {
|
|
|
|
|
tabElement.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updated(changedProperties: Map<string, any>) {
|
|
|
|
|
super.updated(changedProperties);
|
2025-12-30 10:27:34 +00:00
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
if (changedProperties.has('tabs') && this.tabs && this.tabs.length > 0 && !this.selectedTab) {
|
|
|
|
|
this.selectTab(this.tabs[0]);
|
|
|
|
|
}
|
2025-12-30 10:27:34 +00:00
|
|
|
|
2025-06-17 08:41:36 +00:00
|
|
|
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
|
2025-06-27 22:47:24 +00:00
|
|
|
await this.updateComplete;
|
|
|
|
|
// Wait for fonts to load on first update
|
|
|
|
|
if (!this.indicatorInitialized && document.fonts) {
|
|
|
|
|
await document.fonts.ready;
|
|
|
|
|
}
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
this.updateTabIndicator();
|
2025-12-30 10:27:34 +00:00
|
|
|
this.updateScrollState();
|
2025-06-27 22:47:24 +00:00
|
|
|
});
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|
|
|
|
|
}
|
2025-06-27 22:47:24 +00:00
|
|
|
|
|
|
|
|
private indicatorInitialized = false;
|
|
|
|
|
|
|
|
|
|
private updateTabIndicator() {
|
2025-06-27 22:55:20 +00:00
|
|
|
if (!this.shouldShowIndicator()) return;
|
2025-06-27 22:47:24 +00:00
|
|
|
|
2025-06-27 22:55:20 +00:00
|
|
|
const selectedTabElement = this.getSelectedTabElement();
|
|
|
|
|
if (!selectedTabElement) return;
|
|
|
|
|
|
|
|
|
|
const indicator = this.getIndicatorElement();
|
|
|
|
|
if (!indicator) return;
|
|
|
|
|
|
|
|
|
|
this.handleInitialTransition(indicator);
|
|
|
|
|
|
|
|
|
|
if (this.tabStyle === 'horizontal') {
|
|
|
|
|
this.updateHorizontalIndicator(indicator, selectedTabElement);
|
|
|
|
|
} else {
|
|
|
|
|
this.updateVerticalIndicator(indicator, selectedTabElement);
|
2025-06-27 22:47:24 +00:00
|
|
|
}
|
|
|
|
|
|
2025-06-27 22:55:20 +00:00
|
|
|
indicator.style.opacity = '1';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private shouldShowIndicator(): boolean {
|
|
|
|
|
return this.selectedTab && this.showTabIndicator && this.tabs.includes(this.selectedTab);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private getSelectedTabElement(): HTMLElement | null {
|
|
|
|
|
const selectedIndex = this.tabs.indexOf(this.selectedTab);
|
|
|
|
|
const isHorizontal = this.tabStyle === 'horizontal';
|
|
|
|
|
const selector = isHorizontal
|
2025-06-27 22:47:24 +00:00
|
|
|
? `.tabs-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`
|
|
|
|
|
: `.vertical-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`;
|
|
|
|
|
|
2025-06-27 22:55:20 +00:00
|
|
|
return this.shadowRoot.querySelector(selector);
|
|
|
|
|
}
|
2025-06-27 22:47:24 +00:00
|
|
|
|
2025-06-27 22:55:20 +00:00
|
|
|
private getIndicatorElement(): HTMLElement | null {
|
|
|
|
|
return this.shadowRoot.querySelector('.tabIndicator');
|
|
|
|
|
}
|
2025-06-27 22:47:24 +00:00
|
|
|
|
2025-06-27 22:55:20 +00:00
|
|
|
private handleInitialTransition(indicator: HTMLElement): void {
|
2025-06-27 22:47:24 +00:00
|
|
|
if (!this.indicatorInitialized) {
|
|
|
|
|
indicator.classList.add('no-transition');
|
|
|
|
|
this.indicatorInitialized = true;
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
indicator.classList.remove('no-transition');
|
|
|
|
|
}, 50);
|
|
|
|
|
}
|
2025-06-27 22:55:20 +00:00
|
|
|
}
|
2025-06-27 22:47:24 +00:00
|
|
|
|
2025-06-27 22:55:20 +00:00
|
|
|
private updateHorizontalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void {
|
|
|
|
|
const tabContent = tabElement.querySelector('.tab-content') as HTMLElement;
|
|
|
|
|
if (!tabContent) return;
|
2025-06-27 22:47:24 +00:00
|
|
|
|
2025-06-27 22:55:20 +00:00
|
|
|
const wrapperRect = indicator.parentElement.getBoundingClientRect();
|
|
|
|
|
const contentRect = tabContent.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
const contentLeft = contentRect.left - wrapperRect.left;
|
|
|
|
|
const indicatorWidth = contentRect.width + 8;
|
|
|
|
|
const indicatorLeft = contentLeft - 4;
|
|
|
|
|
|
|
|
|
|
indicator.style.width = `${indicatorWidth}px`;
|
|
|
|
|
indicator.style.left = `${indicatorLeft}px`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private updateVerticalIndicator(indicator: HTMLElement, tabElement: HTMLElement): void {
|
|
|
|
|
const tabsContainer = this.shadowRoot.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement;
|
|
|
|
|
if (!tabsContainer) return;
|
|
|
|
|
|
|
|
|
|
indicator.style.top = `${tabElement.offsetTop + tabsContainer.offsetTop}px`;
|
|
|
|
|
indicator.style.height = `${tabElement.clientHeight}px`;
|
2025-06-27 22:47:24 +00:00
|
|
|
}
|
2025-06-17 08:41:36 +00:00
|
|
|
}
|