update
This commit is contained in:
@ -15,13 +15,92 @@ import * as domtools from '@design.estate/dees-domtools';
|
||||
@customElement('dees-appui-tabs')
|
||||
export class DeesAppuiTabs extends DeesElement {
|
||||
public static demo = () => html`
|
||||
<dees-appui-tabs
|
||||
.tabs=${[
|
||||
{ key: 'Tab 1', action: () => console.log('Tab 1 clicked') },
|
||||
{ key: 'Tab 2', action: () => console.log('Tab 2 clicked') },
|
||||
{ key: 'Tab 3', action: () => console.log('Tab 3 clicked') },
|
||||
]}
|
||||
></dees-appui-tabs>
|
||||
<style>
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
padding: 48px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 200px 1fr;
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<div class="section">
|
||||
<div class="section-title">Horizontal Tabs with Animated Indicator</div>
|
||||
<dees-appui-tabs
|
||||
.tabs=${[
|
||||
{ key: 'Home', iconName: 'lucide:home', action: () => console.log('Home clicked') },
|
||||
{ key: 'Analytics Dashboard', iconName: 'lucide:lineChart', action: () => console.log('Analytics clicked') },
|
||||
{ key: 'Reports', iconName: 'lucide:fileText', action: () => console.log('Reports clicked') },
|
||||
{ key: 'User Settings', iconName: 'lucide:settings', action: () => console.log('Settings clicked') },
|
||||
{ key: 'Help', iconName: 'lucide:helpCircle', action: () => console.log('Help clicked') },
|
||||
]}
|
||||
>
|
||||
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
|
||||
Select a tab to see the smooth sliding animation of the indicator. The indicator automatically adjusts its width to match the tab content with minimal padding.
|
||||
</div>
|
||||
</dees-appui-tabs>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Vertical Tabs Layout</div>
|
||||
<div class="two-column">
|
||||
<dees-appui-tabs
|
||||
.tabStyle=${'vertical'}
|
||||
.tabs=${[
|
||||
{ key: 'Profile', iconName: 'lucide:user', action: () => console.log('Profile clicked') },
|
||||
{ key: 'Security', iconName: 'lucide:shield', action: () => console.log('Security clicked') },
|
||||
{ key: 'Notifications', iconName: 'lucide:bell', action: () => console.log('Notifications clicked') },
|
||||
{ key: 'Integrations', iconName: 'lucide:link', action: () => console.log('Integrations clicked') },
|
||||
{ key: 'Advanced', iconName: 'lucide:code', action: () => console.log('Advanced clicked') },
|
||||
]}
|
||||
></dees-appui-tabs>
|
||||
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
|
||||
Vertical tabs work great for settings pages and navigation menus. The animated indicator smoothly transitions between selections.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Without Indicator</div>
|
||||
<dees-appui-tabs
|
||||
.showTabIndicator=${false}
|
||||
.tabs=${[
|
||||
{ key: 'All', action: () => console.log('All clicked') },
|
||||
{ key: 'Active', action: () => console.log('Active clicked') },
|
||||
{ key: 'Completed', action: () => console.log('Completed clicked') },
|
||||
{ key: 'Archived', action: () => console.log('Archived clicked') },
|
||||
]}
|
||||
>
|
||||
<div style="padding: 24px; color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};">
|
||||
Tabs can also be used without the animated indicator by setting showTabIndicator to false.
|
||||
</div>
|
||||
</dees-appui-tabs>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// INSTANCE
|
||||
@ -50,148 +129,217 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
|
||||
.tabs-wrapper {
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#f5f5f5', '#000000')};
|
||||
height: 52px;
|
||||
}
|
||||
|
||||
.tabs-wrapper.horizontal-wrapper {
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
}
|
||||
|
||||
.tabsContainer {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tabsContainer.horizontal {
|
||||
display: grid;
|
||||
padding-top: 20px;
|
||||
padding-bottom: 0px;
|
||||
margin-left: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
height: 48px;
|
||||
padding: 0 16px;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.tabsContainer.horizontal::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tabsContainer.vertical {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
gap: 2px;
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#18181b')};
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
color: ${cssManager.bdTheme('#666', '#a0a0a0')};
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
transition: color 0.1s;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
font-weight: 500;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.horizontal .tab {
|
||||
margin-right: 30px;
|
||||
padding-top: 4px;
|
||||
padding-bottom: 12px;
|
||||
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;
|
||||
}
|
||||
|
||||
.vertical .tab {
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.vertical .tab {
|
||||
padding: 10px 16px;
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
color: ${cssManager.bdTheme('#000', '#ffffff')};
|
||||
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;
|
||||
}
|
||||
|
||||
.vertical .tab:hover {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
|
||||
background: ${cssManager.bdTheme('rgba(244, 244, 245, 0.5)', 'rgba(39, 39, 42, 0.5)')};
|
||||
}
|
||||
|
||||
.tab.selectedTab {
|
||||
color: ${cssManager.bdTheme('#333', '#e0e0e0')};
|
||||
.horizontal .tab.selectedTab {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.horizontal .tab.selectedTab::after,
|
||||
.horizontal .tab.selectedTab + .tab::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.vertical .tab.selectedTab {
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#000', '#ffffff')};
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.tab dees-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.tabs-wrapper .tabIndicator {
|
||||
.tabIndicator {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
left: 40px;
|
||||
bottom: 0px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#161616')};
|
||||
transition: all 0.1s;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#444444')};
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.tabIndicator.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.vertical .tabIndicator {
|
||||
display: 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);
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-top: 20px;
|
||||
padding: 32px 24px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${this.tabStyle === 'horizontal' ? html`
|
||||
<style>
|
||||
.tabsContainer.horizontal {
|
||||
grid-template-columns: repeat(${this.tabs.length}, min-content);
|
||||
}
|
||||
</style>
|
||||
<div class="tabs-wrapper">
|
||||
<div class="tabsContainer horizontal">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${this.showTabIndicator ? html`
|
||||
<div class="tabIndicator"></div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : html`
|
||||
<div class="tabsContainer vertical">
|
||||
${this.tabs.map((tabArg) => {
|
||||
return html`
|
||||
<div
|
||||
class="tab ${tabArg === this.selectedTab ? 'selectedTab' : ''}"
|
||||
@click="${() => this.selectTab(tabArg)}"
|
||||
>
|
||||
${tabArg.iconName ? html`<dees-icon .iconName=${tabArg.iconName}></dees-icon>` : ''}
|
||||
${tabArg.key}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
${this.renderTabsWrapper()}
|
||||
<div class="content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTabsWrapper(): TemplateResult {
|
||||
const isHorizontal = this.tabStyle === 'horizontal';
|
||||
const wrapperClass = isHorizontal ? 'tabs-wrapper horizontal-wrapper' : 'vertical-wrapper';
|
||||
const containerClass = `tabsContainer ${this.tabStyle}`;
|
||||
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTab(tab: interfaces.ITab, isHorizontal: boolean): TemplateResult {
|
||||
const isSelected = tab === this.selectedTab;
|
||||
const classes = `tab ${isSelected ? 'selectedTab' : ''}`;
|
||||
|
||||
const content = isHorizontal ? html`
|
||||
<span class="tab-content">
|
||||
${this.renderTabIcon(tab)}
|
||||
${tab.key}
|
||||
</span>
|
||||
` : html`
|
||||
${this.renderTabIcon(tab)}
|
||||
${tab.key}
|
||||
`;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="${classes}"
|
||||
@click="${() => this.selectTab(tab)}"
|
||||
>
|
||||
${content}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderTabIcon(tab: interfaces.ITab): TemplateResult | '' {
|
||||
return tab.iconName ? html`<dees-icon .icon=${tab.iconName}></dees-icon>` : '';
|
||||
}
|
||||
|
||||
private selectTab(tabArg: interfaces.ITab) {
|
||||
this.selectedTab = tabArg;
|
||||
this.updateTabIndicator();
|
||||
tabArg.action();
|
||||
|
||||
// Emit tab-select event
|
||||
@ -202,31 +350,6 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* updates the indicator position
|
||||
*/
|
||||
private updateTabIndicator() {
|
||||
if (!this.showTabIndicator || this.tabStyle !== 'horizontal' || !this.selectedTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabIndex = this.tabs.indexOf(this.selectedTab);
|
||||
const selectedTabElement: HTMLElement = this.shadowRoot.querySelector(
|
||||
`.tabs-wrapper .tabsContainer .tab:nth-child(${tabIndex + 1})`
|
||||
);
|
||||
|
||||
if (!selectedTabElement) return;
|
||||
|
||||
const tabsContainer: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabsContainer');
|
||||
const marginLeft = parseInt(window.getComputedStyle(tabsContainer).getPropertyValue("margin-left"));
|
||||
const tabIndicator: HTMLElement = this.shadowRoot.querySelector('.tabs-wrapper .tabIndicator');
|
||||
|
||||
if (tabIndicator) {
|
||||
tabIndicator.style.width = selectedTabElement.clientWidth + 24 + 'px';
|
||||
tabIndicator.style.left = selectedTabElement.offsetLeft + marginLeft - 12 + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
if (this.tabs && this.tabs.length > 0) {
|
||||
this.selectTab(this.tabs[0]);
|
||||
@ -241,7 +364,83 @@ export class DeesAppuiTabs extends DeesElement {
|
||||
}
|
||||
|
||||
if (changedProperties.has('selectedTab') || changedProperties.has('tabs')) {
|
||||
this.updateTabIndicator();
|
||||
await this.updateComplete;
|
||||
// Wait for fonts to load on first update
|
||||
if (!this.indicatorInitialized && document.fonts) {
|
||||
await document.fonts.ready;
|
||||
}
|
||||
requestAnimationFrame(() => {
|
||||
this.updateTabIndicator();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private indicatorInitialized = false;
|
||||
|
||||
private updateTabIndicator() {
|
||||
if (!this.selectedTab || !this.showTabIndicator) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selectedIndex = this.tabs.indexOf(this.selectedTab);
|
||||
if (selectedIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Select the correct tab element using nth-child
|
||||
const selector = this.tabStyle === 'horizontal'
|
||||
? `.tabs-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`
|
||||
: `.vertical-wrapper .tabsContainer .tab:nth-child(${selectedIndex + 1})`;
|
||||
|
||||
const selectedTabElement = this.shadowRoot.querySelector(selector) as HTMLElement;
|
||||
|
||||
if (!selectedTabElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const indicator = this.shadowRoot.querySelector('.tabIndicator') as HTMLElement;
|
||||
if (!indicator) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable transition for initial positioning
|
||||
if (!this.indicatorInitialized) {
|
||||
indicator.classList.add('no-transition');
|
||||
this.indicatorInitialized = true;
|
||||
|
||||
setTimeout(() => {
|
||||
indicator.classList.remove('no-transition');
|
||||
}, 50);
|
||||
}
|
||||
|
||||
if (this.tabStyle === 'horizontal') {
|
||||
const tabContent = selectedTabElement.querySelector('.tab-content') as HTMLElement;
|
||||
|
||||
if (tabContent) {
|
||||
// Use getBoundingClientRect for accurate positioning
|
||||
const wrapperRect = indicator.parentElement.getBoundingClientRect();
|
||||
const contentRect = tabContent.getBoundingClientRect();
|
||||
|
||||
// Calculate the position relative to wrapper
|
||||
const contentLeft = contentRect.left - wrapperRect.left;
|
||||
|
||||
// Set width to match content plus small padding
|
||||
const indicatorWidth = contentRect.width + 8;
|
||||
|
||||
// Center the indicator on content (shift left by half the extra width)
|
||||
const indicatorLeft = contentLeft - 4;
|
||||
|
||||
indicator.style.width = `${indicatorWidth}px`;
|
||||
indicator.style.left = `${indicatorLeft}px`;
|
||||
}
|
||||
} else {
|
||||
// For vertical tabs
|
||||
const tabsContainer = this.shadowRoot.querySelector('.vertical-wrapper .tabsContainer') as HTMLElement;
|
||||
|
||||
indicator.style.top = `${selectedTabElement.offsetTop + tabsContainer.offsetTop}px`;
|
||||
indicator.style.height = `${selectedTabElement.clientHeight}px`;
|
||||
}
|
||||
|
||||
indicator.style.opacity = '1';
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user