diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9ffb455e..15dc586e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,30 @@
+## 1.0.0-beta1 (22.03.2021)
+
+### New components:
+
+- [Range](https://mdbootstrap.com/docs/b5/angular/forms/range/)
+- [File](https://mdbootstrap.com/docs/b5/angular/forms/file)
+- [Switch](https://mdbootstrap.com/docs/b5/angular/forms/switch/)
+- [Input group](https://mdbootstrap.com/docs/b5/angular/forms-input-group/)
+- [Pills](https://mdbootstrap.com/docs/b5/angular/navigation/pills/)
+- [Tabs](https://mdbootstrap.com/docs/b5/angular/navigation/tabs/)
+
+### Bug fixes:
+
+- Scrollspy - added `cursor: pointer` styles to scrollspy links,
+- Sidenav - resolved problem with errors when `RouterModule` is not imported,
+- Sidenav - component will be correctly updated on inputs changes,
+- Sidenav - resolved problem with scroll position,
+- Sidenav - added components and module exports to main library index.
+
+### New features:
+
+- Animations - added new animations: `slideLeft`, `slideRight`, `slideUp`, `slideDown`,
+- Sidenav - added focus trap,
+- Sidenav - escape button will now close the component.
+
+---
+
## 1.0.0-alpha4 (08.03.2021)
### New components:
@@ -9,14 +36,18 @@
- [Validation](https://mdbootstrap.com/docs/b5/angular/forms/validation/)
### Bug fixes:
+
- Select - `x options selected` text will be displayed correctly when more than 5 options have been selected,
- Select - fixed clear button focusing issue.
### New features:
+
- Select - added new `displayedLabels` input that allows to change maximum number of comma-separated options labels displayed in the multiselect input,
- Select - added new `optionsSelectedLabel` input that allows to customize x options selected text,
- Select - added new `filterDebounce` input that allows to add delay to the options list updates when using filter input
+---
+
## 1.0.0-alpha3 (22.02.2021)
### New components:
@@ -26,6 +57,8 @@
- [Select](https://mdbootstrap.com/docs/b5/angular/forms/select/)
- [Scrollbar](https://mdbootstrap.com/docs/b5/angular/methods/scrollbar/)
+---
+
## 1.0.0-alpha2 (25.01.2021)
### New components:
@@ -36,6 +69,8 @@
- [Input](https://mdbootstrap.com/docs/b5/angular/forms/input-fields/)
- [Radio](https://mdbootstrap.com/docs/b5/angular/forms/radio/)
+---
+
## 1.0.0-alpha1 (11.01.2021)
The initial release of MDB 5 Angular Alpha 1.
diff --git a/README.txt b/README.txt
index 2d37e158..176e4054 100644
--- a/README.txt
+++ b/README.txt
@@ -1,6 +1,6 @@
MDB 5 Angular
-Version: FREE 1.0.0 Alpha 4
+Version: FREE 1.0.0 Beta 1
Documentation:
https://mdbootstrap.com/docs/b5/angular/
diff --git a/package.json b/package.json
index 58477fd2..0b17e527 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "mdb-angular-ui-kit-free",
- "version": "1.0.0-alpha4",
+ "version": "1.0.0-beta1",
"scripts": {
"ng": "ng",
"start": "ng serve",
diff --git a/projects/mdb-angular-ui-kit/CHANGELOG.md b/projects/mdb-angular-ui-kit/CHANGELOG.md
index 9ffb455e..15dc586e 100644
--- a/projects/mdb-angular-ui-kit/CHANGELOG.md
+++ b/projects/mdb-angular-ui-kit/CHANGELOG.md
@@ -1,3 +1,30 @@
+## 1.0.0-beta1 (22.03.2021)
+
+### New components:
+
+- [Range](https://mdbootstrap.com/docs/b5/angular/forms/range/)
+- [File](https://mdbootstrap.com/docs/b5/angular/forms/file)
+- [Switch](https://mdbootstrap.com/docs/b5/angular/forms/switch/)
+- [Input group](https://mdbootstrap.com/docs/b5/angular/forms-input-group/)
+- [Pills](https://mdbootstrap.com/docs/b5/angular/navigation/pills/)
+- [Tabs](https://mdbootstrap.com/docs/b5/angular/navigation/tabs/)
+
+### Bug fixes:
+
+- Scrollspy - added `cursor: pointer` styles to scrollspy links,
+- Sidenav - resolved problem with errors when `RouterModule` is not imported,
+- Sidenav - component will be correctly updated on inputs changes,
+- Sidenav - resolved problem with scroll position,
+- Sidenav - added components and module exports to main library index.
+
+### New features:
+
+- Animations - added new animations: `slideLeft`, `slideRight`, `slideUp`, `slideDown`,
+- Sidenav - added focus trap,
+- Sidenav - escape button will now close the component.
+
+---
+
## 1.0.0-alpha4 (08.03.2021)
### New components:
@@ -9,14 +36,18 @@
- [Validation](https://mdbootstrap.com/docs/b5/angular/forms/validation/)
### Bug fixes:
+
- Select - `x options selected` text will be displayed correctly when more than 5 options have been selected,
- Select - fixed clear button focusing issue.
### New features:
+
- Select - added new `displayedLabels` input that allows to change maximum number of comma-separated options labels displayed in the multiselect input,
- Select - added new `optionsSelectedLabel` input that allows to customize x options selected text,
- Select - added new `filterDebounce` input that allows to add delay to the options list updates when using filter input
+---
+
## 1.0.0-alpha3 (22.02.2021)
### New components:
@@ -26,6 +57,8 @@
- [Select](https://mdbootstrap.com/docs/b5/angular/forms/select/)
- [Scrollbar](https://mdbootstrap.com/docs/b5/angular/methods/scrollbar/)
+---
+
## 1.0.0-alpha2 (25.01.2021)
### New components:
@@ -36,6 +69,8 @@
- [Input](https://mdbootstrap.com/docs/b5/angular/forms/input-fields/)
- [Radio](https://mdbootstrap.com/docs/b5/angular/forms/radio/)
+---
+
## 1.0.0-alpha1 (11.01.2021)
The initial release of MDB 5 Angular Alpha 1.
diff --git a/projects/mdb-angular-ui-kit/assets/scss/free/_range.scss b/projects/mdb-angular-ui-kit/assets/scss/free/_range.scss
new file mode 100644
index 00000000..fd70d926
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/assets/scss/free/_range.scss
@@ -0,0 +1,45 @@
+// range
+.range {
+ position: relative;
+
+ .thumb {
+ position: absolute;
+ display: block;
+ height: 30px;
+ width: 30px;
+ top: -35px;
+ margin-left: -15px;
+ text-align: center;
+ border-radius: 50% 50% 50% 0;
+ transform: scale(0);
+ transform-origin: bottom;
+ transition: transform 0.2s ease-in-out;
+
+ &:after {
+ position: absolute;
+ display: block;
+ content: '';
+ transform: translateX(-50%);
+ width: 100%;
+ height: 100%;
+ top: 0;
+ border-radius: 50% 50% 50% 0;
+ transform: rotate(-45deg);
+ background: #1266f1;
+ z-index: -1;
+ }
+
+ .thumb-value {
+ display: block;
+ font-size: 12px;
+ line-height: 30px;
+ color: rgb(255, 255, 255);
+ font-weight: 500;
+ z-index: 2;
+ }
+
+ &.thumb-active {
+ transform: scale(1);
+ }
+ }
+}
diff --git a/projects/mdb-angular-ui-kit/assets/scss/free/_scrollspy.scss b/projects/mdb-angular-ui-kit/assets/scss/free/_scrollspy.scss
index 5e596509..3163e79a 100644
--- a/projects/mdb-angular-ui-kit/assets/scss/free/_scrollspy.scss
+++ b/projects/mdb-angular-ui-kit/assets/scss/free/_scrollspy.scss
@@ -1,4 +1,7 @@
// Scrollspy
+.scrollspy-link {
+ cursor: pointer;
+}
.nav-pills {
&.menu-sidebar {
diff --git a/projects/mdb-angular-ui-kit/assets/scss/free/forms/_form-file.scss b/projects/mdb-angular-ui-kit/assets/scss/free/forms/_form-file.scss
new file mode 100644
index 00000000..0cdb5caf
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/assets/scss/free/forms/_form-file.scss
@@ -0,0 +1,19 @@
+.form-control {
+ &[type='file'] {
+ &::-webkit-file-upload-button {
+ background-color: transparent;
+ }
+ }
+ &:hover {
+ &:not(:disabled):not([readonly])::-webkit-file-upload-button {
+ background-color: transparent;
+ }
+ }
+}
+
+.form-control-sm {
+ &::-webkit-file-upload-button {
+ padding: 0.28rem 0.5rem;
+ margin: -0.28rem -0.5rem;
+ }
+}
diff --git a/projects/mdb-angular-ui-kit/assets/scss/free/forms/_form-range.scss b/projects/mdb-angular-ui-kit/assets/scss/free/forms/_form-range.scss
new file mode 100644
index 00000000..e1952dba
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/assets/scss/free/forms/_form-range.scss
@@ -0,0 +1,43 @@
+// Range
+//
+// Style range inputs the same across browsers. Vendor-specific rules for pseudo
+// elements cannot be mixed. As such, there are no shared styles for focus or
+// active states on prefixed selectors.
+
+.form-range {
+ &:focus {
+ box-shadow: none;
+
+ // Pseudo-elements must be split across multiple rulesets to have an effect.
+ // No box-shadow() mixin for focus accessibility.
+ &::-webkit-slider-thumb {
+ box-shadow: none;
+ }
+ &::-moz-range-thumb {
+ box-shadow: none;
+ }
+ &::-ms-thumb {
+ box-shadow: none;
+ }
+ }
+
+ &::-moz-focus-outer {
+ border: 0;
+ }
+
+ &::-webkit-slider-thumb {
+ margin-top: $form-range-webkit-slider-thumb-margin-top; // Webkit specific
+ box-shadow: none;
+ appearance: none;
+ }
+
+ &::-webkit-slider-runnable-track {
+ height: $form-range-webkit-slider-runnable-track-height;
+ border-radius: 0;
+ }
+
+ &::-moz-range-thumb {
+ box-shadow: none;
+ appearance: none;
+ }
+}
diff --git a/projects/mdb-angular-ui-kit/assets/scss/free/forms/_input-group.scss b/projects/mdb-angular-ui-kit/assets/scss/free/forms/_input-group.scss
index f836f813..4cab77c9 100644
--- a/projects/mdb-angular-ui-kit/assets/scss/free/forms/_input-group.scss
+++ b/projects/mdb-angular-ui-kit/assets/scss/free/forms/_input-group.scss
@@ -79,3 +79,13 @@
.input-group > [class*='btn-outline-'] + [class*='btn-outline-'] {
border-left: 0;
}
+
+.input-group div:not(:first-child).dropdown button.dropdown-toggle {
+ margin-left: -$input-border-width;
+ @include border-start-radius(0);
+}
+
+.input-group div:not(:last-child).dropdown button.dropdown-toggle {
+ margin-right: -$input-border-width;
+ @include border-end-radius(0);
+}
diff --git a/projects/mdb-angular-ui-kit/assets/scss/mdb.free.scss b/projects/mdb-angular-ui-kit/assets/scss/mdb.free.scss
index 4840fb24..8e7ff4df 100644
--- a/projects/mdb-angular-ui-kit/assets/scss/mdb.free.scss
+++ b/projects/mdb-angular-ui-kit/assets/scss/mdb.free.scss
@@ -79,9 +79,11 @@
@import './free/tooltip';
@import './free/popover';
@import './free/dropdown';
+@import './free/range';
// MDB FORMS
@import './free/forms/form-check';
@import './free/forms/form-control';
+@import './free/forms/form-range';
@import '~@angular/cdk/overlay-prebuilt.css';
diff --git a/projects/mdb-angular-ui-kit/assets/scss/mdb.scss b/projects/mdb-angular-ui-kit/assets/scss/mdb.scss
index 467c8597..e45c03aa 100644
--- a/projects/mdb-angular-ui-kit/assets/scss/mdb.scss
+++ b/projects/mdb-angular-ui-kit/assets/scss/mdb.scss
@@ -83,11 +83,14 @@
@import './free/ripple';
@import './free/validation';
@import './free/scrollspy';
+@import './free/range';
// MDB FORMS
@import './free/forms/form-check';
@import './free/forms/form-control';
@import './free/forms/form-select';
+@import './free/forms/form-range';
@import './free/forms/input-group';
+@import './free/forms/form-file';
@import '~@angular/cdk/overlay-prebuilt.css';
diff --git a/projects/mdb-angular-ui-kit/checkbox/checkbox.directive.ts b/projects/mdb-angular-ui-kit/checkbox/checkbox.directive.ts
index 7a5d6553..1dcade96 100644
--- a/projects/mdb-angular-ui-kit/checkbox/checkbox.directive.ts
+++ b/projects/mdb-angular-ui-kit/checkbox/checkbox.directive.ts
@@ -91,6 +91,7 @@ export class MdbCheckboxDirective {
}
this._checked = !this._checked;
this.onChange(this.checked);
+ this.onCheckboxChange();
}
onCheckboxChange(): void {
diff --git a/projects/mdb-angular-ui-kit/index.ts b/projects/mdb-angular-ui-kit/index.ts
index ea36b7fc..d832aecd 100644
--- a/projects/mdb-angular-ui-kit/index.ts
+++ b/projects/mdb-angular-ui-kit/index.ts
@@ -13,6 +13,8 @@ import { MdbDropdownModule } from './dropdown/dropdown.module';
import { MdbRippleModule } from './ripple/ripple.module';
import { MdbValidationModule } from './validation/validation.module';
import { MdbScrollspyModule } from './scrollspy/scrollspy.module';
+import { MdbRangeModule } from './range/range.module';
+import { MdbTabsModule } from './tabs/tabs.module';
export { MdbCollapseDirective, MdbCollapseModule } from './collapse/index';
export {
@@ -62,6 +64,14 @@ export {
MdbScrollspyService,
MdbScrollspyModule,
} from './scrollspy/index';
+export { MdbRangeComponent, MdbRangeModule } from './range/index';
+export {
+ MdbTabComponent,
+ MdbTabContentDirective,
+ MdbTabTitleDirective,
+ MdbTabsComponent,
+ MdbTabsModule,
+} from './tabs/index';
const MDB_MODULES = [
MdbCollapseModule,
@@ -75,6 +85,8 @@ const MDB_MODULES = [
MdbRippleModule,
MdbValidationModule,
MdbScrollspyModule,
+ MdbRangeModule,
+ MdbTabsModule,
];
@NgModule({
diff --git a/projects/mdb-angular-ui-kit/package.json b/projects/mdb-angular-ui-kit/package.json
index 02c7ef27..78637c97 100644
--- a/projects/mdb-angular-ui-kit/package.json
+++ b/projects/mdb-angular-ui-kit/package.json
@@ -3,7 +3,7 @@
"repository": "https://github.com/mdbootstrap/mdb-angular-ui-kit",
"author": "MDBootstrap",
"license": "MIT",
- "version": "1.0.0-alpha4",
+ "version": "1.0.0-beta1",
"peerDependencies": {
"@angular/common": "^11.0.0",
"@angular/core": "^11.0.0",
diff --git a/projects/mdb-angular-ui-kit/range/index.ts b/projects/mdb-angular-ui-kit/range/index.ts
new file mode 100644
index 00000000..4aaf8f92
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/range/index.ts
@@ -0,0 +1 @@
+export * from './public_api';
diff --git a/projects/mdb-angular-ui-kit/range/public_api.ts b/projects/mdb-angular-ui-kit/range/public_api.ts
new file mode 100644
index 00000000..6d843762
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/range/public_api.ts
@@ -0,0 +1,2 @@
+export { MdbRangeModule } from './range.module';
+export { MdbRangeComponent } from './range.component';
diff --git a/projects/mdb-angular-ui-kit/range/range.component.html b/projects/mdb-angular-ui-kit/range/range.component.html
new file mode 100644
index 00000000..1db32936
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/range/range.component.html
@@ -0,0 +1,28 @@
+
+
+
+
+ {{ value }}
+
+
diff --git a/projects/mdb-angular-ui-kit/range/range.component.spec.ts b/projects/mdb-angular-ui-kit/range/range.component.spec.ts
new file mode 100644
index 00000000..0e1eea26
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/range/range.component.spec.ts
@@ -0,0 +1,100 @@
+import { Component, OnInit, ViewChild } from '@angular/core';
+import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing';
+import { MdbRangeModule } from './range.module';
+import { MdbRangeComponent } from './range.component';
+import { FormControl, ReactiveFormsModule } from '@angular/forms';
+import { By } from '@angular/platform-browser';
+
+const template = `
+
+`;
+
+@Component({
+ // tslint:disable-next-line: component-selector
+ selector: 'range-test',
+ template,
+})
+class TestRangeComponent implements OnInit {
+ @ViewChild(MdbRangeComponent) _range: MdbRangeComponent;
+
+ rangeControl = new FormControl(50);
+ ngOnInit(): void {
+ this.rangeControl.valueChanges.subscribe((val) => console.log(val));
+ }
+}
+
+describe('MDB Range', () => {
+ let fixture: ComponentFixture;
+ let component: any;
+ let mdbRange: any;
+ let thumb: any;
+ let valueThumb: any;
+ let input: any;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ declarations: [TestRangeComponent],
+ imports: [MdbRangeModule, ReactiveFormsModule],
+ });
+ fixture = TestBed.createComponent(TestRangeComponent);
+ fixture.detectChanges();
+ component = fixture.componentInstance;
+ mdbRange = fixture.debugElement.query(By.css('mdb-range'));
+ thumb = fixture.debugElement.query(By.css('.thumb'));
+ valueThumb = fixture.debugElement.query(By.css('.thumb-value'));
+ input = fixture.debugElement.query(By.css('input'));
+ });
+
+ it('should show thumb on mousedown and hide on mauseup', fakeAsync(() => {
+ expect(thumb.nativeElement.classList.contains('thumb-active')).toBe(false);
+
+ input.nativeElement.dispatchEvent(new MouseEvent('mousedown'));
+
+ fixture.detectChanges();
+ flush();
+
+ expect(thumb.nativeElement.classList.contains('thumb-active')).toBe(true);
+
+ input.nativeElement.dispatchEvent(new MouseEvent('mouseup'));
+
+ fixture.detectChanges();
+ flush();
+
+ expect(thumb.nativeElement.classList.contains('thumb-active')).toBe(false);
+ }));
+
+ it('should show input value', () => {
+ fixture.detectChanges();
+
+ expect(document.querySelector('.thumb')).not.toBe(null);
+ expect(valueThumb.nativeElement.textContent).toBe(input.nativeElement.value);
+ });
+
+ it('should update thumb value after input', () => {
+ input.nativeElement.value = 24;
+ input.nativeElement.dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+
+ expect(valueThumb.nativeElement.textContent).toBe('24');
+ });
+
+ it('should update value after set new FormControl', () => {
+ component.rangeControl = new FormControl(60);
+ fixture.detectChanges();
+
+ expect(valueThumb.nativeElement.textContent).toBe('60');
+ expect(input.nativeElement.value).toBe('60');
+ });
+
+ it('should update thumb position', fakeAsync(() => {
+ const initialThumbStyle = { ...component._range.thumbStyle };
+
+ component.rangeControl = new FormControl(70);
+
+ fixture.detectChanges();
+ flush();
+ const newThumbStyle = { ...component._range.thumbStyle };
+
+ expect(initialThumbStyle.left).not.toBe(newThumbStyle.left);
+ }));
+});
diff --git a/projects/mdb-angular-ui-kit/range/range.component.ts b/projects/mdb-angular-ui-kit/range/range.component.ts
new file mode 100644
index 00000000..0390f818
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/range/range.component.ts
@@ -0,0 +1,111 @@
+import {
+ AfterViewInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ElementRef,
+ EventEmitter,
+ forwardRef,
+ HostListener,
+ Input,
+ Output,
+ ViewChild,
+} from '@angular/core';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+
+export const RANGE_VALUE_ACCESOR: any = {
+ provide: NG_VALUE_ACCESSOR,
+ // tslint:disable-next-line: no-use-before-declare
+ useExisting: forwardRef(() => MdbRangeComponent),
+ multi: true,
+};
+@Component({
+ // tslint:disable-next-line: component-selector
+ selector: 'mdb-range',
+ templateUrl: 'range.component.html',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [RANGE_VALUE_ACCESOR],
+})
+export class MdbRangeComponent implements ControlValueAccessor, AfterViewInit {
+ @ViewChild('input') input: ElementRef;
+ @ViewChild('thumb') thumb: ElementRef;
+ @ViewChild('thumbValue') thumbValue: ElementRef;
+
+ @Input() id: string;
+ @Input() required: boolean;
+ @Input() name: string;
+ @Input() value: string;
+ @Input() disabled: boolean;
+ @Input() min = 0;
+ @Input() max = 100;
+ @Input() step: number;
+ @Input() default: boolean;
+ @Input() defaultRangeCounterClass: string;
+
+ @Output() rangeValueChange = new EventEmitter();
+
+ public visibility = false;
+ public thumbStyle: any;
+
+ @HostListener('change', ['$event']) onchange(event: any): void {
+ this.onChange(event.target.value);
+ }
+
+ @HostListener('input') onInput(): void {
+ this.rangeValueChange.emit({ value: this.value });
+ this.focusRangeInput();
+ }
+
+ constructor(private _cdRef: ChangeDetectorRef) {}
+
+ ngAfterViewInit(): void {
+ this.thumbPositionUpdate();
+ }
+
+ focusRangeInput(): void {
+ this.input.nativeElement.focus();
+ this.visibility = true;
+ }
+
+ blurRangeInput(): void {
+ this.input.nativeElement.blur();
+ this.visibility = false;
+ }
+
+ thumbPositionUpdate(): void {
+ const rangeInput = this.input.nativeElement;
+ const inputValue = rangeInput.value;
+ const minValue = rangeInput.min ? rangeInput.min : 0;
+ const maxValue = rangeInput.max ? rangeInput.max : 100;
+ const newValue = Number(((inputValue - minValue) * 100) / (maxValue - minValue));
+
+ this.value = inputValue;
+ this.thumbStyle = { left: `calc(${newValue}% + (${8 - newValue * 0.15}px))` };
+ }
+
+ // Control Value Accessor Methods
+ onChange = (_: any) => {};
+ onTouched = () => {};
+
+ writeValue(value: any): void {
+ this.value = value;
+
+ this._cdRef.markForCheck();
+
+ setTimeout(() => {
+ this.thumbPositionUpdate();
+ }, 0);
+ }
+
+ registerOnChange(fn: (_: any) => void): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: () => void): void {
+ this.onTouched = fn;
+ }
+
+ setDisabledState(isDisabled: boolean): void {
+ this.disabled = isDisabled;
+ }
+}
diff --git a/projects/mdb-angular-ui-kit/range/range.module.ts b/projects/mdb-angular-ui-kit/range/range.module.ts
new file mode 100644
index 00000000..3347ea45
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/range/range.module.ts
@@ -0,0 +1,12 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FormsModule } from '@angular/forms';
+
+import { MdbRangeComponent } from './range.component';
+
+@NgModule({
+ imports: [CommonModule, FormsModule],
+ declarations: [MdbRangeComponent],
+ exports: [MdbRangeComponent],
+})
+export class MdbRangeModule {}
diff --git a/projects/mdb-angular-ui-kit/scrollspy/scrollspy-link.directive.ts b/projects/mdb-angular-ui-kit/scrollspy/scrollspy-link.directive.ts
index ac23bf03..46b750af 100644
--- a/projects/mdb-angular-ui-kit/scrollspy/scrollspy-link.directive.ts
+++ b/projects/mdb-angular-ui-kit/scrollspy/scrollspy-link.directive.ts
@@ -46,6 +46,9 @@ export class MdbScrollspyLinkDirective implements OnInit {
}
}
+ @HostBinding('class.scrollspy-link')
+ scrollspyLink = true;
+
@HostBinding('class.active')
active = false;
diff --git a/projects/mdb-angular-ui-kit/tabs/index.ts b/projects/mdb-angular-ui-kit/tabs/index.ts
new file mode 100644
index 00000000..4aaf8f92
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/index.ts
@@ -0,0 +1 @@
+export * from './public_api';
diff --git a/projects/mdb-angular-ui-kit/tabs/public_api.ts b/projects/mdb-angular-ui-kit/tabs/public_api.ts
new file mode 100644
index 00000000..f088ab22
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/public_api.ts
@@ -0,0 +1,5 @@
+export { MdbTabComponent } from './tab.component';
+export { MdbTabContentDirective } from './tab-content.directive';
+export { MdbTabTitleDirective } from './tab-title.directive';
+export { MdbTabsComponent } from './tabs.component';
+export { MdbTabsModule } from './tabs.module';
diff --git a/projects/mdb-angular-ui-kit/tabs/tab-content.directive.ts b/projects/mdb-angular-ui-kit/tabs/tab-content.directive.ts
new file mode 100644
index 00000000..2ce9ad5f
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tab-content.directive.ts
@@ -0,0 +1,12 @@
+import { Directive, InjectionToken, TemplateRef } from '@angular/core';
+
+export const MDB_TAB_CONTENT = new InjectionToken('MdbTabContentDirective');
+
+@Directive({
+ // tslint:disable-next-line: directive-selector
+ selector: '[mdbTabContent]',
+ providers: [{ provide: MDB_TAB_CONTENT, useExisting: MdbTabContentDirective }],
+})
+export class MdbTabContentDirective {
+ constructor(public template: TemplateRef) {}
+}
diff --git a/projects/mdb-angular-ui-kit/tabs/tab-outlet.directive.ts b/projects/mdb-angular-ui-kit/tabs/tab-outlet.directive.ts
new file mode 100644
index 00000000..8c645120
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tab-outlet.directive.ts
@@ -0,0 +1,54 @@
+import { CdkPortalOutlet } from '@angular/cdk/portal';
+import { DOCUMENT } from '@angular/common';
+import {
+ ComponentFactoryResolver,
+ Directive,
+ Inject,
+ Input,
+ OnDestroy,
+ OnInit,
+ ViewContainerRef,
+} from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { MdbTabComponent } from './tab.component';
+
+@Directive({
+ // tslint:disable-next-line: directive-selector
+ selector: '[mdbTabPortalOutlet]',
+})
+// tslint:disable-next-line: directive-class-suffix
+export class MdbTabPortalOutlet extends CdkPortalOutlet implements OnInit, OnDestroy {
+ readonly _destroy$: Subject = new Subject();
+
+ @Input() tab: MdbTabComponent;
+
+ constructor(
+ _cfr: ComponentFactoryResolver,
+ _vcr: ViewContainerRef,
+ @Inject(DOCUMENT) _document: any
+ ) {
+ super(_cfr, _vcr, _document);
+ }
+
+ ngOnInit(): void {
+ super.ngOnInit();
+
+ if ((this.tab.shouldAttach || this.tab.active) && !this.hasAttached()) {
+ this.attach(this.tab.content);
+ } else {
+ // tslint:disable-next-line: deprecation
+ this.tab.activeStateChange$.pipe(takeUntil(this._destroy$)).subscribe((isActive) => {
+ if (isActive && !this.hasAttached()) {
+ this.attach(this.tab.content);
+ }
+ });
+ }
+ }
+
+ ngOnDestroy(): void {
+ this._destroy$.next();
+ this._destroy$.complete();
+ super.ngOnDestroy();
+ }
+}
diff --git a/projects/mdb-angular-ui-kit/tabs/tab-title.directive.ts b/projects/mdb-angular-ui-kit/tabs/tab-title.directive.ts
new file mode 100644
index 00000000..87a9bc65
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tab-title.directive.ts
@@ -0,0 +1,12 @@
+import { Directive, InjectionToken, TemplateRef } from '@angular/core';
+
+export const MDB_TAB_TITLE = new InjectionToken('MdbTabTitleDirective');
+
+@Directive({
+ // tslint:disable-next-line: directive-selector
+ selector: '[mdbTabTitle]',
+ providers: [{ provide: MDB_TAB_TITLE, useExisting: MdbTabTitleDirective }],
+})
+export class MdbTabTitleDirective {
+ constructor(public template: TemplateRef) {}
+}
diff --git a/projects/mdb-angular-ui-kit/tabs/tab.component.html b/projects/mdb-angular-ui-kit/tabs/tab.component.html
new file mode 100644
index 00000000..cd48c06b
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tab.component.html
@@ -0,0 +1 @@
+
diff --git a/projects/mdb-angular-ui-kit/tabs/tab.component.ts b/projects/mdb-angular-ui-kit/tabs/tab.component.ts
new file mode 100644
index 00000000..77d69a7d
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tab.component.ts
@@ -0,0 +1,92 @@
+import { TemplatePortal } from '@angular/cdk/portal';
+import {
+ Component,
+ ContentChild,
+ ElementRef,
+ Input,
+ OnInit,
+ Renderer2,
+ TemplateRef,
+ ViewChild,
+ ViewContainerRef,
+} from '@angular/core';
+import { Subject } from 'rxjs';
+import { MDB_TAB_CONTENT } from './tab-content.directive';
+import { MDB_TAB_TITLE } from './tab-title.directive';
+
+@Component({
+ // tslint:disable-next-line: component-selector
+ selector: 'mdb-tab',
+ templateUrl: './tab.component.html',
+})
+export class MdbTabComponent implements OnInit {
+ @ContentChild(MDB_TAB_CONTENT, { read: TemplateRef, static: true })
+ _lazyContent: TemplateRef;
+
+ @ContentChild(MDB_TAB_TITLE, { read: TemplateRef, static: true })
+ _titleContent: TemplateRef;
+
+ @ViewChild(TemplateRef, { static: true }) _content: TemplateRef;
+
+ readonly activeStateChange$: Subject = new Subject();
+
+ @Input() disabled = false;
+ @Input() title: string;
+
+ get active(): boolean {
+ return this._active;
+ }
+
+ get content(): TemplatePortal | null {
+ return this._contentPortal;
+ }
+
+ get titleContent(): TemplatePortal | null {
+ return this._titlePortal;
+ }
+
+ get shouldAttach(): boolean {
+ return this._lazyContent === undefined;
+ }
+
+ private _contentPortal: TemplatePortal | null = null;
+ private _titlePortal: TemplatePortal | null = null;
+
+ // tslint:disable-next-line: adjacent-overload-signatures
+ set active(value: boolean) {
+ if (value) {
+ this._renderer.addClass(this._elementRef.nativeElement, 'show');
+ this._renderer.addClass(this._elementRef.nativeElement, 'active');
+ } else {
+ this._renderer.removeClass(this._elementRef.nativeElement, 'show');
+ this._renderer.removeClass(this._elementRef.nativeElement, 'active');
+ }
+
+ this._active = value;
+ this.activeStateChange$.next(value);
+ }
+ private _active = false;
+
+ constructor(
+ private _elementRef: ElementRef,
+ private _renderer: Renderer2,
+ private _vcr: ViewContainerRef
+ ) {}
+
+ ngOnInit(): void {
+ this._createContentPortal();
+
+ if (this._titleContent) {
+ this._createTitlePortal();
+ }
+ }
+
+ private _createContentPortal(): void {
+ const content = this._lazyContent || this._content;
+ this._contentPortal = new TemplatePortal(content, this._vcr);
+ }
+
+ private _createTitlePortal(): void {
+ this._titlePortal = new TemplatePortal(this._titleContent, this._vcr);
+ }
+}
diff --git a/projects/mdb-angular-ui-kit/tabs/tabs.component.html b/projects/mdb-angular-ui-kit/tabs/tabs.component.html
new file mode 100644
index 00000000..5fd3e207
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tabs.component.html
@@ -0,0 +1,54 @@
+
+
+
diff --git a/projects/mdb-angular-ui-kit/tabs/tabs.component.ts b/projects/mdb-angular-ui-kit/tabs/tabs.component.ts
new file mode 100644
index 00000000..0bbb014f
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tabs.component.ts
@@ -0,0 +1,109 @@
+import {
+ AfterContentInit,
+ Component,
+ ContentChildren,
+ EventEmitter,
+ HostBinding,
+ Input,
+ OnDestroy,
+ Output,
+ QueryList,
+} from '@angular/core';
+import { Subject } from 'rxjs';
+import { takeUntil } from 'rxjs/operators';
+import { MdbTabComponent } from './tab.component';
+
+export class MdbTabChange {
+ index: number;
+ tab: MdbTabComponent;
+}
+
+@Component({
+ // tslint:disable-next-line: component-selector
+ selector: 'mdb-tabs',
+ templateUrl: './tabs.component.html',
+})
+export class MdbTabsComponent implements AfterContentInit, OnDestroy {
+ @ContentChildren(MdbTabComponent) tabs: QueryList;
+
+ readonly _destroy$: Subject = new Subject();
+
+ @Input() fill = false;
+ @Input() justified = false;
+ @Input() pills = false;
+
+ @HostBinding('class.row')
+ @Input()
+ vertical = false;
+
+ private _selectedIndex: number;
+
+ @Output() activeTabChange: EventEmitter = new EventEmitter();
+
+ constructor() {}
+
+ ngAfterContentInit(): void {
+ const firstActiveTabIndex = this.tabs.toArray().findIndex((tab) => !tab.disabled);
+
+ this.setActiveTab(firstActiveTabIndex);
+ // tslint:disable-next-line: deprecation
+ this.tabs.changes.pipe(takeUntil(this._destroy$)).subscribe(() => {
+ const hasActiveTab = this.tabs.find((tab) => tab.active);
+
+ if (!hasActiveTab) {
+ const closestTabIndex = this._getClosestTabIndex(this._selectedIndex);
+
+ if (closestTabIndex !== -1) {
+ this.setActiveTab(closestTabIndex);
+ }
+ }
+ });
+ }
+
+ setActiveTab(index: number): void {
+ const activeTab = this.tabs.toArray()[index];
+
+ if (!activeTab || (activeTab && activeTab.disabled)) {
+ return;
+ }
+
+ this.tabs.forEach((tab) => (tab.active = tab === activeTab));
+ this._selectedIndex = index;
+
+ const tabChangeEvent = this._getTabChangeEvent(index, activeTab);
+ this.activeTabChange.emit(tabChangeEvent);
+ }
+
+ private _getTabChangeEvent(index: number, tab: MdbTabComponent): MdbTabChange {
+ const event = new MdbTabChange();
+ event.index = index;
+ event.tab = tab;
+
+ return event;
+ }
+
+ private _getClosestTabIndex(index: number): number {
+ const tabs = this.tabs.toArray();
+ const tabsLength = tabs.length;
+ if (!tabsLength) {
+ return -1;
+ }
+
+ for (let i = 1; i <= tabsLength; i += 1) {
+ const prevIndex = index - i;
+ const nextIndex = index + i;
+ if (tabs[prevIndex] && !tabs[prevIndex].disabled) {
+ return prevIndex;
+ }
+ if (tabs[nextIndex] && !tabs[nextIndex].disabled) {
+ return nextIndex;
+ }
+ }
+ return -1;
+ }
+
+ ngOnDestroy(): void {
+ this._destroy$.next();
+ this._destroy$.complete();
+ }
+}
diff --git a/projects/mdb-angular-ui-kit/tabs/tabs.module.ts b/projects/mdb-angular-ui-kit/tabs/tabs.module.ts
new file mode 100644
index 00000000..679136a3
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tabs.module.ts
@@ -0,0 +1,27 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { MdbTabComponent } from './tab.component';
+import { MdbTabsComponent } from './tabs.component';
+import { PortalModule } from '@angular/cdk/portal';
+import { MdbTabContentDirective } from './tab-content.directive';
+import { MdbTabPortalOutlet } from './tab-outlet.directive';
+import { MdbTabTitleDirective } from './tab-title.directive';
+
+@NgModule({
+ declarations: [
+ MdbTabComponent,
+ MdbTabContentDirective,
+ MdbTabTitleDirective,
+ MdbTabPortalOutlet,
+ MdbTabsComponent,
+ ],
+ imports: [CommonModule, PortalModule],
+ exports: [
+ MdbTabComponent,
+ MdbTabContentDirective,
+ MdbTabTitleDirective,
+ MdbTabPortalOutlet,
+ MdbTabsComponent,
+ ],
+})
+export class MdbTabsModule {}
diff --git a/projects/mdb-angular-ui-kit/tabs/tabs.spec.ts b/projects/mdb-angular-ui-kit/tabs/tabs.spec.ts
new file mode 100644
index 00000000..79e0e5cb
--- /dev/null
+++ b/projects/mdb-angular-ui-kit/tabs/tabs.spec.ts
@@ -0,0 +1,118 @@
+import { Component, QueryList, ViewChild, ViewChildren } from '@angular/core';
+import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { MdbTabComponent } from './tab.component';
+import { MdbTabsComponent } from './tabs.component';
+import { MdbTabsModule } from './tabs.module';
+
+const tabsTemplate = `
+
+ Tab 1 content
+ Tab 2 content
+ Tab 3 content
+
+`;
+
+@Component({
+ template: tabsTemplate,
+})
+export class TabsTestComponent {
+ pills = false;
+ fill = false;
+ justified = false;
+
+ @ViewChild(MdbTabsComponent) tabsComponent: MdbTabsComponent;
+ @ViewChildren(MdbTabComponent) tabComponents: QueryList;
+}
+
+describe('NbTabsetComponent', () => {
+ let fixture: ComponentFixture;
+ let component: TabsTestComponent;
+ let tabsComponent: MdbTabsComponent;
+
+ beforeEach(fakeAsync(() => {
+ TestBed.configureTestingModule({
+ declarations: [TabsTestComponent],
+ imports: [MdbTabsModule],
+ });
+
+ fixture = TestBed.createComponent(TabsTestComponent);
+ component = fixture.componentInstance;
+
+ fixture.detectChanges();
+ tick();
+
+ tabsComponent = component.tabsComponent;
+ }));
+
+ it('should activate first available tab', () => {
+ fixture.detectChanges();
+
+ const tabs = component.tabComponents.toArray();
+
+ expect(tabs[0].active).toBe(false);
+ expect(tabs[1].active).toBe(true);
+ });
+
+ it('should change active tab on tab button click', () => {
+ fixture.detectChanges();
+
+ const tabs = component.tabComponents.toArray();
+ const tabLinks = fixture.debugElement.queryAll(By.css('.nav-link'));
+
+ expect(tabs[1].active).toBe(true);
+
+ tabLinks[2].nativeElement.click();
+ fixture.detectChanges();
+
+ expect(tabs[2].active).toBe(true);
+ });
+
+ it('should add active class to active tab link', () => {
+ fixture.detectChanges();
+
+ const tabLinks = fixture.debugElement.queryAll(By.css('.nav-link'));
+
+ expect(tabLinks[1].nativeElement.classList.contains('active')).toBe(true);
+
+ tabLinks[2].nativeElement.click();
+ fixture.detectChanges();
+
+ expect(tabLinks[2].nativeElement.classList.contains('active')).toBe(true);
+ });
+
+ it('should add disabled class to disabled tab link', () => {
+ fixture.detectChanges();
+
+ const tabLinks = fixture.debugElement.queryAll(By.css('.nav-link'));
+
+ expect(tabLinks[0].nativeElement.classList.contains('disabled')).toBe(true);
+ });
+
+ it('should add nav-pills class if pills input is set to true', () => {
+ component.pills = true;
+ fixture.detectChanges();
+
+ const tabNav = fixture.debugElement.query(By.css('.nav'));
+
+ expect(tabNav.nativeElement.classList.contains('nav-pills')).toBe(true);
+ });
+
+ it('should add nav-fill class if fill input is set to true', () => {
+ component.fill = true;
+ fixture.detectChanges();
+
+ const tabNav = fixture.debugElement.query(By.css('.nav'));
+
+ expect(tabNav.nativeElement.classList.contains('nav-fill')).toBe(true);
+ });
+
+ it('should add nav-justified class if justified input is set to true', () => {
+ component.justified = true;
+ fixture.detectChanges();
+
+ const tabNav = fixture.debugElement.query(By.css('.nav'));
+
+ expect(tabNav.nativeElement.classList.contains('nav-justified')).toBe(true);
+ });
+});