Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
<div
[class.row]="vertical"
[class.flex-row-reverse]="inverted && vertical"
[class.d-flex]="inverted && !vertical"
[class.flex-column-reverse]="inverted && !vertical">
[class.row]="isVertical"
[class.flex-row-reverse]="inverted && isVertical"
[class.d-flex]="inverted && !isVertical"
[class.flex-column-reverse]="inverted && !isVertical">
<div
[class.col-5]="inverted && vertical"
[class.col-md-4]="inverted && vertical"
[class.col-lg-3]="inverted && vertical"
[class.col-4]="!inverted && vertical"
[class.col-md-3]="!inverted && vertical">
[class.col-5]="inverted && isVertical"
[class.col-md-4]="inverted && isVertical"
[class.col-lg-3]="inverted && isVertical"
[class.col-4]="!inverted && isVertical"
[class.col-md-3]="!inverted && isVertical">
@if (tabs) {
<ul
class="nav nav-tabs"
[class.nav-tabs-editable]="editable"
[class.nav-tabs-cards]="cards"
[class.nav-tabs-vertical]="vertical"
[class.nav-tabs-vertical]="isVertical"
[class.auto]="auto"
[class.nav-tabs-icon-text]="iconText"
[class.nav-dark]="dark"
Expand Down Expand Up @@ -54,11 +54,11 @@
}
</div>
<div
[class.col-7]="inverted && vertical"
[class.col-md-8]="inverted && vertical"
[class.col-lg-9]="inverted && vertical"
[class.col-8]="!inverted && vertical"
[class.col-md-9]="!inverted && vertical">
[class.col-7]="inverted && isVertical"
[class.col-md-8]="inverted && isVertical"
[class.col-lg-9]="inverted && isVertical"
[class.col-8]="!inverted && isVertical"
[class.col-md-9]="!inverted && isVertical">
@if (tabs) {
<div class="tab-content">
@for (tab of tabs; track tab.id) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,43 @@ describe('ItTabContainerComponent', () => {
it('should create', () => {
expect(component).toBeTruthy();
});

describe('isVertical', () => {
it('should return false when vertical is not set', () => {
expect(component.isVertical).toBe(false);
});

it('should return true when vertical is set without breakpoint', () => {
component.vertical = true;
expect(component.isVertical).toBe(true);
});

it('should accept verticalBreakpoint input', () => {
component.vertical = true;
component.verticalBreakpoint = 'md';
expect(component.verticalBreakpoint).toBe('md');
});

it('should be responsive when vertical and verticalBreakpoint are both set', () => {
component.vertical = true;
component.verticalBreakpoint = 'md';
// After ngAfterViewInit, isVertical should reflect the media query state
component.ngAfterViewInit();
// The value depends on the test viewport, but the getter should not throw
expect(typeof component.isVertical).toBe('boolean');
});
});

describe('cleanup', () => {
it('should not throw on destroy with responsive breakpoint', () => {
component.vertical = true;
component.verticalBreakpoint = 'lg';
component.ngAfterViewInit();
expect(() => component.ngOnDestroy()).not.toThrow();
});

it('should not throw on destroy without responsive breakpoint', () => {
expect(() => component.ngOnDestroy()).not.toThrow();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ import { NgTemplateOutlet } from '@angular/common';
import { ItIconComponent } from '../../../utils/icon/icon.component';
import { inputToBoolean } from '../../../../utils/coercion';

/** Bootstrap Italia breakpoint names and their minimum widths in px. */
export type BootstrapBreakpoint = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';

const BREAKPOINT_PX: Record<BootstrapBreakpoint, number> = {
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1400,
};

@Component({
selector: 'it-tab-container',
templateUrl: './tab-container.component.html',
Expand Down Expand Up @@ -48,10 +59,26 @@ export class ItTabContainerComponent extends ItAbstractComponent implements OnDe
@Input({ transform: inputToBoolean }) cards?: boolean;

/**
* Show vertical navigation
* Show vertical navigation.
* When used together with `verticalBreakpoint`, this acts as the initial state
* and becomes responsive — vertical above the breakpoint, horizontal below it.
*/
@Input({ transform: inputToBoolean }) vertical?: boolean;

/**
* When set, the tab container switches from vertical to horizontal layout
* below the specified Bootstrap breakpoint.
* Requires `vertical` to be set to `true`.
*
* @example
* ```html
* <it-tab-container vertical verticalBreakpoint="md">
* <!-- vertical on md+ screens, horizontal on smaller -->
* </it-tab-container>
* ```
*/
@Input() verticalBreakpoint?: BootstrapBreakpoint;

/**
* The tab position
*/
Expand All @@ -75,7 +102,18 @@ export class ItTabContainerComponent extends ItAbstractComponent implements OnDe

@Output() tabAdded = new EventEmitter();

/** Computed vertical state (respects responsive breakpoint). */
get isVertical(): boolean {
if (this.verticalBreakpoint && this.vertical) {
return this._isVerticalResponsive;
}
return !!this.vertical;
}

private tabSubscriptions?: Array<Subscription>;
private _mediaQueryList?: MediaQueryList;
private _mediaHandler?: (e: MediaQueryListEvent) => void;
private _isVerticalResponsive = true;

constructor() {
super();
Expand All @@ -84,6 +122,8 @@ export class ItTabContainerComponent extends ItAbstractComponent implements OnDe
override ngAfterViewInit(): void {
super.ngAfterViewInit();

this._setupResponsiveBreakpoint();

this.tabs?.changes
.pipe(
// When tabs changes (dynamic add/remove)
Expand Down Expand Up @@ -119,6 +159,7 @@ export class ItTabContainerComponent extends ItAbstractComponent implements OnDe

ngOnDestroy(): void {
this.tabSubscriptions?.forEach(sub => sub.unsubscribe());
this._teardownResponsiveBreakpoint();
}

onTab(tab: ItTabItemComponent) {
Expand All @@ -133,4 +174,32 @@ export class ItTabContainerComponent extends ItAbstractComponent implements OnDe
$event.preventDefault();
this.tabAdded.emit();
}

private _setupResponsiveBreakpoint(): void {
if (!this.verticalBreakpoint || !this.vertical) {
return;
}

const minWidth = BREAKPOINT_PX[this.verticalBreakpoint];
if (typeof window === 'undefined' || !window.matchMedia) {
return;
}

this._mediaQueryList = window.matchMedia(`(min-width: ${minWidth}px)`);
this._isVerticalResponsive = this._mediaQueryList.matches;

this._mediaHandler = (e: MediaQueryListEvent) => {
this._isVerticalResponsive = e.matches;
this._changeDetectorRef.detectChanges();
};
this._mediaQueryList.addEventListener('change', this._mediaHandler);
}

private _teardownResponsiveBreakpoint(): void {
if (this._mediaQueryList && this._mediaHandler) {
this._mediaQueryList.removeEventListener('change', this._mediaHandler);
this._mediaQueryList = undefined;
this._mediaHandler = undefined;
}
}
}
13 changes: 13 additions & 0 deletions src/app/tabs/tabs-examples/tabs-examples.component.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,16 @@

<it-source-display html="{$ sanitize(htmlDynamic) $}" typescript="{$ sanitize(typescriptDynamic) $}" >
</it-source-display>


{% set htmlResponsive %}
{% include "../tabs-responsive-example/tabs-responsive-example.component.html" %}
{% endset %}

{% set typescriptResponsive %}
{% include "../tabs-responsive-example/tabs-responsive-example.component.ts" %}
{% endset %}

<it-tabs-responsive-example></it-tabs-responsive-example>

<it-source-display html="{$ sanitize(htmlResponsive) $}" typescript="{$ sanitize(typescriptResponsive) $}"></it-source-display>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<h4>Vertical responsive (breakpoint md)</h4>
<p>Le tab diventano verticali sopra il breakpoint <code>md</code> (768px) e orizzontali sotto.</p>

<it-tab-container vertical verticalBreakpoint="md">
<it-tab-item label="Tab 1" active>
<p>Contenuto del primo tab — verticale sopra md, orizzontale sotto.</p>
</it-tab-item>
<it-tab-item label="Tab 2">
<p>Contenuto del secondo tab.</p>
</it-tab-item>
<it-tab-item label="Tab 3">
<p>Contenuto del terzo tab.</p>
</it-tab-item>
</it-tab-container>

<h4 class="mt-4">Vertical responsive (breakpoint lg)</h4>
<p>Breakpoint <code>lg</code> (992px): verticale sopra, orizzontale sotto.</p>

<it-tab-container vertical verticalBreakpoint="lg">
<it-tab-item label="Tab A" active>
<p>Layout verticale sopra lg.</p>
</it-tab-item>
<it-tab-item label="Tab B">
<p>Layout orizzontale sotto lg.</p>
</it-tab-item>
</it-tab-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Component } from '@angular/core';

@Component({
selector: 'it-tabs-responsive-example',
templateUrl: './tabs-responsive-example.component.html',
standalone: false,
})
export class TabsResponsiveExampleComponent {}
9 changes: 8 additions & 1 deletion src/app/tabs/tabs.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,16 @@ import { TabsExampleComponent } from './tabs-example/tabs-example.component';
import { TabsExamplesComponent } from './tabs-examples/tabs-examples.component';
import { TabsIndexComponent } from './tabs-index/tabs-index.component';
import { TabsDynamicExampleComponent } from './tabs-dynamic-example/tabs-dynamic-example.component';
import { TabsResponsiveExampleComponent } from './tabs-responsive-example/tabs-responsive-example.component';

@NgModule({
imports: [CommonModule, FormsModule, ReactiveFormsModule, SharedModule, TabsRoutingModule],
declarations: [TabsExampleComponent, TabsExamplesComponent, TabsIndexComponent, TabsDynamicExampleComponent],
declarations: [
TabsExampleComponent,
TabsExamplesComponent,
TabsIndexComponent,
TabsDynamicExampleComponent,
TabsResponsiveExampleComponent,
],
})
export class TabsModule {}
Loading