Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 142 additions & 25 deletions packages/core/src/components/tabs/tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@ import {
iconChevronRightSmall,
} from '@siemens/ix-icons/icons';

type ManagedClass =
(typeof TAB_MANAGED_CLASSES)[keyof typeof TAB_MANAGED_CLASSES];

const TAB_MANAGED_CLASSES = {
SELECTED: 'selected',
DISABLED: 'disabled',
SMALL_TAB: 'small-tab',
ICON: 'icon',
STRETCHED: 'stretched',
BOTTOM: 'bottom',
TOP: 'top',
CIRCLE: 'circle',
HYDRATED: 'hydrated',
} as const;

const MANAGED_CLASSES_SET = new Set(
Object.values(TAB_MANAGED_CLASSES) as ManagedClass[]
);

@Component({
tag: 'ix-tabs',
styleUrl: 'tabs.scss',
Expand Down Expand Up @@ -86,6 +105,8 @@ export class Tabs {

private windowStartSize = window.innerWidth;
private resizeObserver?: ResizeObserver;
private classObserver?: MutationObserver;
private updateScheduled = false;

private clickAction: {
timeout: NodeJS.Timeout | null;
Expand Down Expand Up @@ -127,6 +148,123 @@ export class Tabs {
this.resizeObserver.observe(parentElement);
}

private observeSlotChanges() {
this.classObserver?.disconnect();

this.classObserver = new MutationObserver(() => {
this.scheduleTabUpdate();
});

this.classObserver.observe(this.hostElement, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class'],
});
}

private scheduleTabUpdate() {
if (this.updateScheduled) return;
this.updateScheduled = true;

requestAnimationFrame(() => {
this.updateTabAttributes();
this.updateScheduled = false;
});
}

private setTabAttributes(element: HTMLIxTabItemElement, index: number) {
const isSelected = index === this.selected;
const isDisabled = element.disabled;

if (this.small) element.setAttribute('small', 'true');

if (this.rounded) element.setAttribute('rounded', 'true');

element.setAttribute('layout', this.layout);
element.setAttribute('selected', isSelected.toString());
element.setAttribute('placement', this.placement);
element.toggleAttribute('disabled', isDisabled);

this.applyRequiredClasses(element, isSelected, isDisabled);
}

private applyRequiredClasses(
element: HTMLIxTabItemElement,
isSelected: boolean,
isDisabled: boolean
) {
const existingClasses = Array.from(element.classList);
const customClasses = existingClasses.filter(
(className) => !MANAGED_CLASSES_SET.has(className as ManagedClass)
);
const requiredClasses = this.buildRequiredClasses(isSelected, isDisabled);

element.className = [...customClasses, ...requiredClasses].join(' ');
}

private buildRequiredClasses(
isSelected: boolean,
isDisabled: boolean
): string[] {
const classConditions = {
[TAB_MANAGED_CLASSES.HYDRATED]: true,
[TAB_MANAGED_CLASSES.SELECTED]: isSelected,
[TAB_MANAGED_CLASSES.DISABLED]: isDisabled,
[TAB_MANAGED_CLASSES.SMALL_TAB]: this.small,
[TAB_MANAGED_CLASSES.STRETCHED]: this.layout === 'stretched',
[TAB_MANAGED_CLASSES.BOTTOM]: this.placement === 'bottom',
[TAB_MANAGED_CLASSES.TOP]: this.placement === 'top',
[TAB_MANAGED_CLASSES.CIRCLE]: this.rounded,
};

return Object.entries(classConditions)
.filter(([, condition]) => condition)
.map(([className]) => className);
}

private ensureSelectedIndex() {
if (this.totalItems === 0) {
console.warn('ix-tabs: No tabs available for selection');
this.selected = -1;
return;
}

if (this.selected < this.totalItems) {
return;
}

const originalIndex = this.selected;
const previousIndex = originalIndex - 1;

if (previousIndex >= 0 && previousIndex < this.totalItems) {
this.updateSelected(previousIndex);
return;
}

if (this.totalItems > 0) {
this.updateSelected(0);
}
}

private updateSelected(index: number) {
this.selected = index;
this.selectedChange.emit(index);
}

private updateTabAttributes() {
const tabs = this.getTabs();
this.totalItems = tabs.length;

this.ensureSelectedIndex();

tabs.forEach((element, index) => {
this.setTabAttributes(element, index);
});

this.renderArrows();
}

private showArrows() {
try {
const tabWrapper = this.getTabsWrapper();
Expand Down Expand Up @@ -270,35 +408,11 @@ export class Tabs {
}

componentWillLoad() {
const tabs = this.getTabs();

tabs.map((element, index) => {
if (this.small) element.setAttribute('small', 'true');

if (this.rounded) element.setAttribute('rounded', 'true');

element.setAttribute('layout', this.layout);
element.setAttribute(
'selected',
index === this.selected ? 'true' : 'false'
);

element.setAttribute('placement', this.placement);
});

this.initResizeObserver();
}

componentDidRender() {
const tabs = this.getTabs();
this.totalItems = tabs.length;

tabs.map((element, index) => {
element.setAttribute(
'selected',
index === this.selected ? 'true' : 'false'
);
});
this.updateTabAttributes();
}

componentWillRender() {
Expand All @@ -319,10 +433,13 @@ export class Tabs {
this.dragStart(element, event)
);
});

this.observeSlotChanges();
}

disconnectedCallback() {
this.resizeObserver?.disconnect();
this.classObserver?.disconnect();
}

@Listen('tabClick')
Expand Down
Loading
Loading