From d86edce97376e0c243c695fb10868010f80292dc Mon Sep 17 00:00:00 2001 From: Artyom Zankevich Date: Fri, 9 Aug 2019 19:57:27 +0300 Subject: [PATCH] feat(toggle): add toggle component (#1304) --- DEV_DOCS.md | 2 +- docs/assets/images/components/toggle.svg | 1 + docs/structure.ts | 8 + e2e/toggle.e2e-spec.ts | 24 ++ src/app/playground-components.ts | 41 +++ .../components/checkbox/checkbox.component.ts | 2 +- .../toggle/_toggle.component.theme.scss | 130 +++++++ .../components/toggle/toggle.component.scss | 59 ++++ .../components/toggle/toggle.component.ts | 333 ++++++++++++++++++ .../theme/components/toggle/toggle.module.ts | 21 ++ .../theme/components/toggle/toggle.spec.ts | 137 +++++++ src/framework/theme/public_api.ts | 2 + .../theme/styles/global/_components.scss | 2 + .../theme/styles/themes/_mapping.scss | 80 +++++ .../toggle/toggle-disabled.component.ts | 21 ++ .../toggle/toggle-form.component.ts | 27 ++ .../toggle/toggle-label-position.component.ts | 24 ++ .../toggle/toggle-routing.module.ts | 47 +++ .../toggle/toggle-showcase.component.ts | 20 ++ .../toggle/toggle-status.component.ts | 24 ++ .../toggle/toggle-test.component.ts | 28 ++ .../with-layout/toggle/toggle.module.ts | 35 ++ .../with-layout/with-layout-routing.module.ts | 4 + 23 files changed, 1070 insertions(+), 2 deletions(-) create mode 100644 docs/assets/images/components/toggle.svg create mode 100644 e2e/toggle.e2e-spec.ts create mode 100644 src/framework/theme/components/toggle/_toggle.component.theme.scss create mode 100644 src/framework/theme/components/toggle/toggle.component.scss create mode 100644 src/framework/theme/components/toggle/toggle.component.ts create mode 100644 src/framework/theme/components/toggle/toggle.module.ts create mode 100644 src/framework/theme/components/toggle/toggle.spec.ts create mode 100644 src/playground/with-layout/toggle/toggle-disabled.component.ts create mode 100644 src/playground/with-layout/toggle/toggle-form.component.ts create mode 100644 src/playground/with-layout/toggle/toggle-label-position.component.ts create mode 100644 src/playground/with-layout/toggle/toggle-routing.module.ts create mode 100644 src/playground/with-layout/toggle/toggle-showcase.component.ts create mode 100644 src/playground/with-layout/toggle/toggle-status.component.ts create mode 100644 src/playground/with-layout/toggle/toggle-test.component.ts create mode 100644 src/playground/with-layout/toggle/toggle.module.ts diff --git a/DEV_DOCS.md b/DEV_DOCS.md index 4536811a35..c8a33d7d09 100644 --- a/DEV_DOCS.md +++ b/DEV_DOCS.md @@ -429,7 +429,7 @@ _your-component.component.theme.scss (optional, styles that depends on theme var - register your component in framework ```` -src/framework/theme/index.ts (add exports of your component and module) +src/framework/theme/public_api.ts (add exports of your component and module) src/framework/theme/styles/global/_components.scss (if you create _your-component.component.theme.scss you have to register mixin) ```` diff --git a/docs/assets/images/components/toggle.svg b/docs/assets/images/components/toggle.svg new file mode 100644 index 0000000000..628423ba76 --- /dev/null +++ b/docs/assets/images/components/toggle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/structure.ts b/docs/structure.ts index e1aa71d677..2318e88da6 100644 --- a/docs/structure.ts +++ b/docs/structure.ts @@ -475,6 +475,14 @@ export const structure = [ 'NbCheckboxComponent', ], }, + { + type: 'tabs', + name: 'Toggle', + icon: 'toggle.svg', + source: [ + 'NbToggleComponent', + ], + }, { type: 'tabs', name: 'Radio', diff --git a/e2e/toggle.e2e-spec.ts b/e2e/toggle.e2e-spec.ts new file mode 100644 index 0000000000..a5855b4069 --- /dev/null +++ b/e2e/toggle.e2e-spec.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { browser, by, element } from 'protractor'; + +describe('nb-toggle', () => { + beforeEach((done) => { + browser.get('#/toggle/toggle-test.component').then(() => done()); + }); + + it('should turn on on click', () => { + const input = element(by.css('#first input')); + const indicator = element(by.css('#first .toggle')); + + expect(input.getAttribute('checked')).toBeFalsy(); + indicator.click(); + expect(input.getAttribute('checked')).toBeTruthy(); + indicator.click(); + expect(input.getAttribute('checked')).toBeFalsy(); + }); +}); diff --git a/src/app/playground-components.ts b/src/app/playground-components.ts index d606ddc59a..0f1edd6ed0 100644 --- a/src/app/playground-components.ts +++ b/src/app/playground-components.ts @@ -1323,6 +1323,47 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [ }, ], }, + { + path: 'toggle', + children: [ + { + path: 'toggle-disabled.component', + link: '/toggle/toggle-disabled.component', + component: 'ToggleDisabledComponent', + name: 'Toggle Disabled', + }, + { + path: 'toggle-showcase.component', + link: '/toggle/toggle-showcase.component', + component: 'ToggleShowcaseComponent', + name: 'Toggle Showcase', + }, + { + path: 'toggle-status.component', + link: '/toggle/toggle-status.component', + component: 'ToggleStatusComponent', + name: 'Toggle Status', + }, + { + path: 'toggle-test.component', + link: '/toggle/toggle-test.component', + component: 'ToggleTestComponent', + name: 'Toggle Test', + }, + { + path: 'toggle-label-position.component', + link: '/toggle/toggle-label-position.component', + component: 'ToggleLabelPositionComponent', + name: 'Toggle Label Position', + }, + { + path: 'toggle-form.component', + link: '/toggle/toggle-form.component', + component: 'ToggleFormComponent', + name: 'Toggle Form', + }, + ], + }, { path: 'context-menu', children: [ diff --git a/src/framework/theme/components/checkbox/checkbox.component.ts b/src/framework/theme/components/checkbox/checkbox.component.ts index 9a27454ec7..69d1b87920 100644 --- a/src/framework/theme/components/checkbox/checkbox.component.ts +++ b/src/framework/theme/components/checkbox/checkbox.component.ts @@ -198,7 +198,7 @@ export class NbCheckboxComponent implements ControlValueAccessor { /** * Checkbox status. - * Possible values are: `primary` (default), `success`, `warning`, `danger`, `info` + * Possible values are: `primary`, `success`, `warning`, `danger`, `info` */ @Input() status: '' | NbComponentStatus = ''; diff --git a/src/framework/theme/components/toggle/_toggle.component.theme.scss b/src/framework/theme/components/toggle/_toggle.component.theme.scss new file mode 100644 index 0000000000..705e6965ca --- /dev/null +++ b/src/framework/theme/components/toggle/_toggle.component.theme.scss @@ -0,0 +1,130 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +@mixin nb-toggle-theme() { + nb-toggle { + .toggle { + height: nb-theme(toggle-height); + width: nb-theme(toggle-width); + background-color: nb-theme(toggle-background-color); + border: nb-theme(toggle-border-width) solid nb-theme(toggle-border-color); + border-radius: nb-theme(toggle-border-radius); + cursor: nb-theme(toggle-cursor); + } + + .toggle.checked { + background-color: nb-theme(toggle-primary-checked-background-color); + border-color: nb-theme(toggle-primary-checked-border-color); + } + + .native-input { + &:focus + .toggle { + border-color: nb-theme(toggle-primary-focus-border-color); + box-shadow: 0 0 0 nb-theme(toggle-outline-width) nb-theme(toggle-outline-color); + } + + &:active + .toggle { + background-color: nb-theme(toggle-primary-active-background-color); + border-color: nb-theme(toggle-primary-active-border-color); + } + } + + .toggle:hover { + background-color: nb-theme(toggle-primary-hover-background-color); + border-color: nb-theme(toggle-primary-hover-border-color); + } + + .native-input:disabled { + & + .toggle { + background-color: nb-theme(toggle-disabled-background-color); + border-color: nb-theme(toggle-disabled-border-color); + cursor: nb-theme(toggle-disabled-cursor); + + nb-icon { + color: nb-theme(toggle-disabled-switcher-checkmark-color); + } + } + + & ~ .text { + color: nb-theme(toggle-disabled-text-color); + } + } + + .toggle-switcher { + width: nb-theme(toggle-switcher-size); + height: nb-theme(toggle-switcher-size); + background-color: nb-theme(toggle-switcher-background-color); + + nb-icon { + color: nb-theme(toggle-switcher-checkmark-color); + } + } + + .text { + color: nb-theme(toggle-text-color); + font-family: nb-theme(toggle-text-font-family); + font-size: nb-theme(toggle-text-font-size); + font-weight: nb-theme(toggle-text-font-weight); + line-height: nb-theme(toggle-text-line-height); + } + } + + @each $status in nb-get-statuses() { + @include nb-toggle-status($status); + } +} + +@mixin nb-toggle-status($status: '') { + nb-toggle.status-#{$status} { + + .toggle { + background-color: nb-theme(toggle-#{$status}-background-color); + border-color: nb-theme(toggle-#{$status}-border-color); + } + + .toggle.checked { + background-color: nb-theme(toggle-#{$status}-checked-background-color); + border-color: nb-theme(toggle-#{$status}-checked-border-color); + } + + .native-input { + &:focus + .toggle { + border-color: nb-theme(toggle-#{$status}-focus-border-color); + } + + &:active + .toggle { + background-color: nb-theme(toggle-#{$status}-active-background-color); + border-color: nb-theme(toggle-#{$status}-active-border-color); + } + } + + .toggle:hover { + background-color: nb-theme(toggle-#{$status}-hover-background-color); + border-color: nb-theme(toggle-#{$status}-hover-border-color); + } + + .toggle-switcher { + nb-icon { + color: nb-theme(toggle-#{$status}-checked-switcher-checkmark-color); + } + } + + .native-input:disabled { + & + .toggle { + background-color: nb-theme(toggle-disabled-background-color); + border-color: nb-theme(toggle-disabled-border-color); + + nb-icon { + color: nb-theme(toggle-disabled-switcher-checkmark-color); + } + } + + & ~ .text { + color: nb-theme(toggle-disabled-text-color); + } + } + } +} diff --git a/src/framework/theme/components/toggle/toggle.component.scss b/src/framework/theme/components/toggle/toggle.component.scss new file mode 100644 index 0000000000..48d52487b8 --- /dev/null +++ b/src/framework/theme/components/toggle/toggle.component.scss @@ -0,0 +1,59 @@ +@import '../../styles/core/mixins'; + +:host { + display: inline-flex; + outline: none; +} + +:host(.toggle-label-left) .text { + padding-right: 0.6875rem; + @include nb-ltr(order, -1); + @include nb-rtl(order, 1); +} + +:host(.toggle-label-right) .text { + padding-left: 0.6875rem; + @include nb-ltr(order, 1); + @include nb-rtl(order, -1); +} + +:host(.toggle-label-start) .toggle-label { + flex-direction: row-reverse; + + .text { + @include nb-ltr(padding-right, 0.6875rem); + @include nb-rtl(padding-left, 0.6875rem); + } +} + +:host(.toggle-label-end) .text { + @include nb-ltr(padding-left, 0.6875rem); + @include nb-rtl(padding-right, 0.6875rem); +} + +.toggle-label { + position: relative; + display: inline-flex; + align-items: center; +} + +.toggle { + position: relative; + display: inline-flex; + box-sizing: content-box; + @include nb-component-animation(background-color, border, box-shadow); +} + +.toggle-switcher { + position: absolute; + border-radius: 50%; + margin: 1px; + + nb-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 40%; + } +} diff --git a/src/framework/theme/components/toggle/toggle.component.ts b/src/framework/theme/components/toggle/toggle.component.ts new file mode 100644 index 0000000000..4be25c5344 --- /dev/null +++ b/src/framework/theme/components/toggle/toggle.component.ts @@ -0,0 +1,333 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { + Component, + Input, + HostBinding, + forwardRef, + ChangeDetectorRef, + OnInit, + Output, + EventEmitter, + OnDestroy, + ChangeDetectionStrategy, +} from '@angular/core'; +import { trigger, state, style, animate, transition } from '@angular/animations'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; +import { NbLayoutDirectionService, NbLayoutDirection } from '../../services/direction.service'; +import { NbComponentStatus } from '../component-status'; + +import { convertToBoolProperty } from '../helpers'; + +/** + * Styled toggle component + * + * @stacked-example(Showcase, toggle/toggle-showcase.component) + * + * ### Installation + * + * Import `NbToggleComponent` to your feature module. + * ```ts + * @NgModule({ + * imports: [ + * // ... + * NbToggleModule, + * ], + * }) + * export class PageModule { } + * ``` + * ### Usage + * + * Toggle may have one of the following statuses: `primary`, `success`, `warning`, `danger`, `info` + * + * @stacked-example(Colored Toggles, toggle/toggle-status.component) + * + * You can disable toggle + * + * @stacked-example(Disabled Toggles, toggle/toggle-disabled.component) + * + * Toggle may have a label with following positions: `left`, `right`, `start`, `end` (default) + * + * @stacked-example(Toggles With Labels, toggle/toggle-label-position.component.ts) + * + * You can set checked state via `checked` binding: + * + * ```html + * + * ``` + * + * Or you can set control value via reactive forms or ngModel bindings: + * + * @stacked-example(Toggle form binding, toggle/toggle-form.component) + * + * @styles + * + * toggle-height: + * toggle-width: + * toggle-border-width: + * toggle-border-radius: + * toggle-border-color: + * toggle-background-color: + * toggle-outline-width: + * toggle-outline-color: + * toggle-switcher-size: + * toggle-switcher-background-color: + * toggle-switcher-checkmark-color: + * toggle-text-color: + * toggle-text-font-family: + * toggle-text-font-size: + * toggle-text-font-weight: + * toggle-text-line-height: + * toggle-cursor: + * toggle-disabled-background-color: + * toggle-disabled-border-color: + * toggle-disabled-switcher-checkmark-color: + * toggle-disabled-text-color: + * toggle-disabled-cursor: + * toggle-primary-background-color: + * toggle-primary-border-color: + * toggle-primary-checked-background-color: + * toggle-primary-checked-border-color: + * toggle-primary-checked-switcher-checkmark-color: + * toggle-primary-focus-border-color: + * toggle-primary-hover-background-color: + * toggle-primary-hover-border-color: + * toggle-primary-active-background-color: + * toggle-primary-active-border-color: + * toggle-success-background-color: + * toggle-success-border-color: + * toggle-success-checked-background-color: + * toggle-success-checked-border-color: + * toggle-success-checked-switcher-checkmark-color: + * toggle-success-focus-border-color: + * toggle-success-hover-background-color: + * toggle-success-hover-border-color: + * toggle-success-active-background-color: + * toggle-success-active-border-color: + * toggle-info-background-color: + * toggle-info-border-color: + * toggle-info-checked-background-color: + * toggle-info-checked-border-color: + * toggle-info-checked-switcher-checkmark-color: + * toggle-info-focus-border-color: + * toggle-info-hover-background-color: + * toggle-info-hover-border-color: + * toggle-info-active-background-color: + * toggle-info-active-border-color: + * toggle-warning-background-color: + * toggle-warning-border-color: + * toggle-warning-checked-background-color: + * toggle-warning-checked-border-color: + * toggle-warning-checked-switcher-checkmark-color: + * toggle-warning-focus-border-color: + * toggle-warning-hover-background-color: + * toggle-warning-hover-border-color: + * toggle-warning-active-background-color: + * toggle-warning-active-border-color: + * toggle-danger-background-color: + * toggle-danger-border-color: + * toggle-danger-checked-background-color: + * toggle-danger-checked-border-color: + * toggle-danger-checked-switcher-checkmark-color: + * toggle-danger-focus-border-color: + * toggle-danger-hover-background-color: + * toggle-danger-hover-border-color: + * toggle-danger-active-background-color: + * toggle-danger-active-border-color: + */ +@Component({ + selector: 'nb-toggle', + animations: [ + trigger('onOff', [ + state('ltrOn', style({ right: 0 })), + state('rtlOn', style({ left: 0 })), + transition(':enter', [animate(0)]), + transition('* <=> *', [animate('0.15s')]), + ]), + ], + template: ` + + `, + styleUrls: [ `./toggle.component.scss` ], + providers: [{ + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => NbToggleComponent), + multi: true, + }], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class NbToggleComponent implements OnInit, OnDestroy, ControlValueAccessor { + + onChange: any = () => { }; + onTouched: any = () => { }; + + private destroy$ = new Subject(); + + /** + * Toggle checked + * @type {boolean} + */ + @Input() + get checked(): boolean { + return this._checked; + } + set checked(value: boolean) { + this._checked = value; + } + private _checked: boolean = false; + + /** + * Controls input disabled state + */ + @Input() + get disabled(): boolean { + return this._disabled; + } + set disabled(value: boolean) { + this._disabled = convertToBoolProperty(value); + } + private _disabled: boolean = false; + + /** + * Toggle status. + * Possible values are: `primary`, `success`, `warning`, `danger`, `info` + */ + @Input() + status: '' | NbComponentStatus = ''; + + /** + * Toggle label position. + * Possible values are: `left`, `right`, `start`, `end` (default) + */ + @Input() labelPosition: 'left' | 'right' | 'start' | 'end' = 'end'; + + /** + * Output when checked state is changed by a user + * @type EventEmitter + */ + @Output() checkedChange = new EventEmitter(); + + @HostBinding('class.status-primary') + get primary() { + return this.status === 'primary'; + } + + @HostBinding('class.status-success') + get success() { + return this.status === 'success'; + } + + @HostBinding('class.status-warning') + get warning() { + return this.status === 'warning'; + } + + @HostBinding('class.status-danger') + get danger() { + return this.status === 'danger'; + } + + @HostBinding('class.status-info') + get info() { + return this.status === 'info'; + } + + @HostBinding('class.toggle-label-left') + get labelLeft() { + return this.labelPosition === 'left'; + } + + @HostBinding('class.toggle-label-right') + get labelRight() { + return this.labelPosition === 'right'; + } + + @HostBinding('class.toggle-label-start') + get labelStart() { + return this.labelPosition === 'start'; + } + + @HostBinding('class.toggle-label-end') + get labelEnd() { + return this.labelPosition === 'end'; + } + + constructor( + private changeDetector: ChangeDetectorRef, + private layoutDirection: NbLayoutDirectionService, + ) {} + + ngOnInit(): void { + this.layoutDirection.onDirectionChange() + .pipe(takeUntil(this.destroy$)) + .subscribe(() => this.changeDetector.detectChanges()); + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + checkState(): string { + if (this.checked) { + if (this.layoutDirection.getDirection() === NbLayoutDirection.LTR) { + return 'ltrOn'; + } else { + return 'rtlOn'; + } + } + } + + registerOnChange(fn: any) { + this.onChange = fn; + } + + registerOnTouched(fn: any) { + this.onTouched = fn; + } + + writeValue(val: any) { + this.checked = val; + this.changeDetector.markForCheck(); + } + + setDisabledState(val: boolean) { + this.disabled = convertToBoolProperty(val); + this.changeDetector.markForCheck(); + } + + updateValue(event: Event): void { + const input = (event.target as HTMLInputElement); + this.checked = input.checked; + this.checkedChange.emit(this.checked); + this.onChange(this.checked); + } + + onInputClick(event: Event) { + event.stopPropagation(); + } +} diff --git a/src/framework/theme/components/toggle/toggle.module.ts b/src/framework/theme/components/toggle/toggle.module.ts new file mode 100644 index 0000000000..cb1f04b571 --- /dev/null +++ b/src/framework/theme/components/toggle/toggle.module.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NbIconModule } from '../icon/icon.module'; + +import { NbToggleComponent } from './toggle.component'; + +@NgModule({ + imports: [ + CommonModule, + NbIconModule, + ], + declarations: [NbToggleComponent], + exports: [NbToggleComponent], +}) +export class NbToggleModule { } diff --git a/src/framework/theme/components/toggle/toggle.spec.ts b/src/framework/theme/components/toggle/toggle.spec.ts new file mode 100644 index 0000000000..fc3efc11a1 --- /dev/null +++ b/src/framework/theme/components/toggle/toggle.spec.ts @@ -0,0 +1,137 @@ +import { NbToggleModule } from '@nebular/theme/components/toggle/toggle.module'; +import { NbToggleComponent } from './toggle.component'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, DebugElement } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { ReactiveFormsModule, FormControl } from '@angular/forms'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { NbLayoutDirectionService } from '../../services/direction.service'; + + +describe('Component: NbToggle', () => { + let toggle: NbToggleComponent; + let fixture: ComponentFixture; + let toggleInput: DebugElement; + let testContainerEl: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [BrowserAnimationsModule, NbToggleModule], + providers: [NbLayoutDirectionService], + }); + + fixture = TestBed.createComponent(NbToggleComponent); + toggle = fixture.componentInstance; + testContainerEl = fixture.elementRef.nativeElement; + + toggleInput = fixture.debugElement.query(By.css('input')); + }); + + it('Setting `disabled` to `true` disables toggle input', () => { + toggle.disabled = true; + fixture.detectChanges(); + expect(toggleInput.nativeElement.disabled).toBeTruthy(); + }); + + it('Setting `disabled` to `false` enables toggle input', () => { + toggle.disabled = false; + fixture.detectChanges(); + expect(toggleInput.nativeElement.disabled).toBeFalsy(); + }); + + it('Setting `checked` to `true` makes toggle input on', () => { + toggle.checked = true; + fixture.detectChanges(); + expect(toggleInput.nativeElement.checked).toBeTruthy(); + }); + + it('Setting `checked` to `false` makes toggle input off', () => { + toggle.checked = false; + fixture.detectChanges(); + expect(toggleInput.nativeElement.checked).toBeFalsy(); + }); + + it('Setting `status` to `primary` apply corresponding class to host element', () => { + toggle.status = 'primary'; + fixture.detectChanges(); + expect(testContainerEl.classList.contains('status-primary')).toBeTruthy(); + }); + + it('Setting `status` to `success` apply corresponding class to host element', () => { + toggle.status = 'success'; + fixture.detectChanges(); + expect(testContainerEl.classList.contains('status-success')).toBeTruthy(); + }); + + it('Setting `status` to `warning` apply corresponding class to host element', () => { + toggle.status = 'warning'; + fixture.detectChanges(); + expect(testContainerEl.classList.contains('status-warning')).toBeTruthy(); + }); + + it('Setting `status` to `danger` apply corresponding class to host element', () => { + toggle.status = 'danger'; + fixture.detectChanges(); + expect(testContainerEl.classList.contains('status-danger')).toBeTruthy(); + }); + + it('Setting `status` to `info` apply corresponding class to host element', () => { + toggle.status = 'info'; + fixture.detectChanges(); + expect(testContainerEl.classList.contains('status-info')).toBeTruthy(); + }); +}); + +// Test component with reactive forms +@Component({ + template: ``, +}) +class ToggleWithFormControlComponent { + formControl = new FormControl(); +} + +describe('Component: NbToggle with form control', () => { + let fixture: ComponentFixture; + let toggleComponent: DebugElement; + let toggleInstance: NbToggleComponent; + let testComponent: ToggleWithFormControlComponent; + let inputElement: HTMLInputElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ReactiveFormsModule, BrowserAnimationsModule, NbToggleModule], + providers: [NbLayoutDirectionService], + declarations: [ToggleWithFormControlComponent], + }); + + fixture = TestBed.createComponent(ToggleWithFormControlComponent); + fixture.detectChanges(); + + toggleComponent = fixture.debugElement.query( + By.directive(NbToggleComponent), + ); + + toggleInstance = toggleComponent.componentInstance; + testComponent = fixture.debugElement.componentInstance; + + inputElement = ( + toggleComponent.nativeElement.querySelector('input') + ); + }); + + it('Toggling form control `disabled` state properly applied', () => { + expect(toggleInstance.disabled).toBeFalsy(); + + testComponent.formControl.disable(); + fixture.detectChanges(); + + expect(toggleInstance.disabled).toBeTruthy(); + expect(inputElement.disabled).toBeTruthy(); + + testComponent.formControl.enable(); + fixture.detectChanges(); + + expect(toggleInstance.disabled).toBeFalsy(); + expect(inputElement.disabled).toBeFalsy(); + }); +}); diff --git a/src/framework/theme/public_api.ts b/src/framework/theme/public_api.ts index 14a1a5099b..59f3da2f89 100644 --- a/src/framework/theme/public_api.ts +++ b/src/framework/theme/public_api.ts @@ -205,3 +205,5 @@ export * from './components/icon/icon.component'; export * from './components/icon/icon'; export * from './components/icon/icon-pack'; export * from './components/icon/icon-libraries'; +export * from './components/toggle/toggle.module'; +export * from './components/toggle/toggle.component'; diff --git a/src/framework/theme/styles/global/_components.scss b/src/framework/theme/styles/global/_components.scss index e000132e8b..1479c6c6c3 100644 --- a/src/framework/theme/styles/global/_components.scss +++ b/src/framework/theme/styles/global/_components.scss @@ -18,6 +18,7 @@ @import '../../components/actions/actions.component.theme'; @import '../../components/search/search.component.theme'; @import '../../components/checkbox/checkbox.component.theme'; +@import '../../components/toggle/toggle.component.theme'; @import '../../components/progress-bar/progress-bar.component.theme'; @import '../../components/badge/badge.component.theme'; @import '../../components/alert/alert.component.theme'; @@ -56,6 +57,7 @@ @include nb-search-theme(); @include nb-spinner-theme(); @include nb-checkbox-theme(); + @include nb-toggle-theme(); @include nb-progress-bar-theme(); @include nb-badge-theme(); @include nb-stepper-theme(); diff --git a/src/framework/theme/styles/themes/_mapping.scss b/src/framework/theme/styles/themes/_mapping.scss index f7ab0d6e49..dff793f7b8 100644 --- a/src/framework/theme/styles/themes/_mapping.scss +++ b/src/framework/theme/styles/themes/_mapping.scss @@ -1761,4 +1761,84 @@ $eva-mapping: ( icon-success-color: color-success-default, icon-warning-color: color-warning-default, icon-danger-color: color-danger-default, + + toggle-height: 1.875rem, + toggle-width: 3.125rem, + toggle-border-width: 1px, + toggle-border-radius: 100px, + toggle-border-color: border-basic-color-4, + toggle-background-color: background-basic-color-3, + toggle-outline-width: outline-width, + toggle-outline-color: outline-color, + toggle-switcher-size: 1.75rem, + toggle-switcher-background-color: background-basic-color-1, + toggle-switcher-checkmark-color: color-primary-default, + toggle-text-color: text-basic-color, + toggle-text-font-family: text-subtitle-2-font-family, + toggle-text-font-size: text-subtitle-2-font-size, + toggle-text-font-weight: text-subtitle-2-font-weight, + toggle-text-line-height: text-subtitle-2-line-height, + toggle-cursor: pointer, + + toggle-disabled-background-color: background-basic-color-2, + toggle-disabled-border-color: border-basic-color-3, + toggle-disabled-switcher-checkmark-color: border-basic-color-3, + toggle-disabled-text-color: text-disabled-color, + toggle-disabled-cursor: default, + + toggle-primary-background-color: background-basic-color-2, + toggle-primary-border-color: color-primary-default, + toggle-primary-checked-background-color: color-primary-default, + toggle-primary-checked-border-color: color-primary-default, + toggle-primary-checked-switcher-checkmark-color: color-primary-default, + toggle-primary-focus-border-color: color-primary-700, + toggle-primary-hover-background-color: color-primary-400, + toggle-primary-hover-border-color: color-primary-400, + toggle-primary-active-background-color: color-primary-600, + toggle-primary-active-border-color: color-primary-600, + + toggle-success-background-color: background-basic-color-2, + toggle-success-border-color: color-success-default, + toggle-success-checked-background-color: color-success-default, + toggle-success-checked-border-color: color-success-default, + toggle-success-checked-switcher-checkmark-color: color-success-default, + toggle-success-focus-border-color: color-success-700, + toggle-success-hover-background-color: color-success-400, + toggle-success-hover-border-color: color-success-400, + toggle-success-active-background-color: color-success-600, + toggle-success-active-border-color: color-success-600, + + toggle-info-background-color: background-basic-color-2, + toggle-info-border-color: color-info-default, + toggle-info-checked-background-color: color-info-default, + toggle-info-checked-border-color: color-info-default, + toggle-info-checked-switcher-checkmark-color: color-info-default, + toggle-info-focus-border-color: color-info-700, + toggle-info-hover-background-color: color-info-400, + toggle-info-hover-border-color: color-info-400, + toggle-info-active-background-color: color-info-600, + toggle-info-active-border-color: color-info-600, + + toggle-warning-background-color: background-basic-color-2, + toggle-warning-border-color: color-warning-default, + toggle-warning-checked-background-color: color-warning-default, + toggle-warning-checked-border-color: color-warning-default, + toggle-warning-checked-switcher-checkmark-color: color-warning-default, + toggle-warning-focus-border-color: color-warning-700, + toggle-warning-hover-background-color: color-warning-400, + toggle-warning-hover-border-color: color-warning-400, + toggle-warning-active-background-color: color-warning-600, + toggle-warning-active-border-color: color-warning-600, + + toggle-danger-background-color: background-basic-color-2, + toggle-danger-border-color: color-danger-default, + toggle-danger-checked-background-color: color-danger-default, + toggle-danger-checked-border-color: color-danger-default, + toggle-danger-checked-switcher-checkmark-color: color-danger-default, + toggle-danger-focus-border-color: color-danger-700, + toggle-danger-hover-background-color: color-danger-400, + toggle-danger-hover-border-color: color-danger-400, + toggle-danger-active-background-color: color-danger-600, + toggle-danger-active-border-color: color-danger-600, + ); diff --git a/src/playground/with-layout/toggle/toggle-disabled.component.ts b/src/playground/with-layout/toggle/toggle-disabled.component.ts new file mode 100644 index 0000000000..83d8cf79ca --- /dev/null +++ b/src/playground/with-layout/toggle/toggle-disabled.component.ts @@ -0,0 +1,21 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-toggle-disabled', + template: ` + + + + + + + `, +}) +export class ToggleDisabledComponent { +} diff --git a/src/playground/with-layout/toggle/toggle-form.component.ts b/src/playground/with-layout/toggle/toggle-form.component.ts new file mode 100644 index 0000000000..ba3daf1f72 --- /dev/null +++ b/src/playground/with-layout/toggle/toggle-form.component.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +@Component({ + selector: 'nb-toggle-form', + template: ` + + + Toggle with NgModel + Toggle with FormControl + + + `, +}) +export class ToggleFormComponent { + + toggleNgModel = true; + + toggleFormControl = new FormControl(); + +} diff --git a/src/playground/with-layout/toggle/toggle-label-position.component.ts b/src/playground/with-layout/toggle/toggle-label-position.component.ts new file mode 100644 index 0000000000..92b826c31e --- /dev/null +++ b/src/playground/with-layout/toggle/toggle-label-position.component.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-toggle-label-position', + template: ` + + + Label Start + Label End + Label Right + Label Left + Label Default Disabled + + + `, +}) +export class ToggleLabelPositionComponent { +} diff --git a/src/playground/with-layout/toggle/toggle-routing.module.ts b/src/playground/with-layout/toggle/toggle-routing.module.ts new file mode 100644 index 0000000000..17e707da92 --- /dev/null +++ b/src/playground/with-layout/toggle/toggle-routing.module.ts @@ -0,0 +1,47 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { NgModule } from '@angular/core'; +import { RouterModule, Route} from '@angular/router'; +import { ToggleDisabledComponent } from './toggle-disabled.component'; +import { ToggleFormComponent } from './toggle-form.component'; +import { ToggleLabelPositionComponent } from './toggle-label-position.component'; +import { ToggleShowcaseComponent } from './toggle-showcase.component'; +import { ToggleStatusComponent } from './toggle-status.component'; +import { ToggleTestComponent } from './toggle-test.component'; + +const routes: Route[] = [ + { + path: 'toggle-disabled.component', + component: ToggleDisabledComponent, + }, + { + path: 'toggle-showcase.component', + component: ToggleShowcaseComponent, + }, + { + path: 'toggle-status.component', + component: ToggleStatusComponent, + }, + { + path: 'toggle-test.component', + component: ToggleTestComponent, + }, + { + path: 'toggle-label-position.component', + component: ToggleLabelPositionComponent, + }, + { + path: 'toggle-form.component', + component: ToggleFormComponent, + }, +]; + +@NgModule({ + imports: [ RouterModule.forChild(routes) ], + exports: [ RouterModule ], +}) +export class ToggleRoutingModule {} diff --git a/src/playground/with-layout/toggle/toggle-showcase.component.ts b/src/playground/with-layout/toggle/toggle-showcase.component.ts new file mode 100644 index 0000000000..7d7a744932 --- /dev/null +++ b/src/playground/with-layout/toggle/toggle-showcase.component.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-toggle-showcase', + template: ` + + + + + + `, +}) +export class ToggleShowcaseComponent { +} diff --git a/src/playground/with-layout/toggle/toggle-status.component.ts b/src/playground/with-layout/toggle/toggle-status.component.ts new file mode 100644 index 0000000000..48f5785741 --- /dev/null +++ b/src/playground/with-layout/toggle/toggle-status.component.ts @@ -0,0 +1,24 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-toggle-status', + template: ` + + + + + + + + + + `, +}) +export class ToggleStatusComponent { +} diff --git a/src/playground/with-layout/toggle/toggle-test.component.ts b/src/playground/with-layout/toggle/toggle-test.component.ts new file mode 100644 index 0000000000..373b8aaa00 --- /dev/null +++ b/src/playground/with-layout/toggle/toggle-test.component.ts @@ -0,0 +1,28 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component } from '@angular/core'; + +@Component({ + selector: 'nb-app-toggle-test', + template: ` + + + + + + + + + + + + + + `, +}) +export class ToggleTestComponent { +} diff --git a/src/playground/with-layout/toggle/toggle.module.ts b/src/playground/with-layout/toggle/toggle.module.ts new file mode 100644 index 0000000000..57a6f9ec01 --- /dev/null +++ b/src/playground/with-layout/toggle/toggle.module.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { NbToggleModule, NbCardModule } from '@nebular/theme'; +import { ToggleFormComponent } from './toggle-form.component'; +import { ToggleLabelPositionComponent } from './toggle-label-position.component'; +import { ToggleRoutingModule } from './toggle-routing.module'; +import { ToggleDisabledComponent } from './toggle-disabled.component'; +import { ToggleShowcaseComponent } from './toggle-showcase.component'; +import { ToggleStatusComponent } from './toggle-status.component'; +import { ToggleTestComponent } from './toggle-test.component'; + +@NgModule({ + declarations: [ + ToggleDisabledComponent, + ToggleShowcaseComponent, + ToggleStatusComponent, + ToggleTestComponent, + ToggleLabelPositionComponent, + ToggleFormComponent, + ], + imports: [ + FormsModule, + ReactiveFormsModule, + NbCardModule, + NbToggleModule, + ToggleRoutingModule, + ], +}) +export class ToggleModule {} diff --git a/src/playground/with-layout/with-layout-routing.module.ts b/src/playground/with-layout/with-layout-routing.module.ts index ebe202eccd..f67a1ed750 100644 --- a/src/playground/with-layout/with-layout-routing.module.ts +++ b/src/playground/with-layout/with-layout-routing.module.ts @@ -149,6 +149,10 @@ const routes: Route[] = [ path: 'icon', loadChildren: './icon/icon.module#IconModule', }, + { + path: 'toggle', + loadChildren: './toggle/toggle.module#ToggleModule', + }, ], }, ];