Skip to content

Commit

Permalink
fix(option group): propagate disabled state to child options (#1416)
Browse files Browse the repository at this point in the history
  • Loading branch information
yggg committed May 27, 2019
1 parent e10ab2f commit 54da610
Show file tree
Hide file tree
Showing 6 changed files with 304 additions and 6 deletions.
4 changes: 2 additions & 2 deletions scripts/ci/browserstack/stop-tunnel.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ set -e -o pipefail

echo "Shutting down Browserstack tunnel"

killall BrowserStackLocal
pkill BrowserStack

while [[ -n `ps -ef | grep "BrowserStackLocal" | grep -v "grep"` ]]; do
printf "."
sleep .5
done

echo ""
echo "Browserstack tunnel has been shut down"
echo "Browserstack tunnel has been shut down"
148 changes: 148 additions & 0 deletions src/framework/theme/components/select/option-group.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, flush, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { RouterTestingModule } from '@angular/router/testing';
import {
NbLayoutModule,
NbOptionComponent,
NbOptionGroupComponent,
NbSelectModule,
NbSelectComponent,
NbThemeModule,
} from '@nebular/theme';

@Component({
template: `
<nb-layout>
<nb-layout-column>
<nb-select [disabled]="selectDisabled">
<nb-option-group [disabled]="optionGroupDisabled" [title]="optionGroupTitle">
<nb-option *ngIf="showOption" [value]="1" [disabled]="optionDisabled">1</nb-option>
</nb-option-group>
</nb-select>
</nb-layout-column>
</nb-layout>
`,
})
export class NbOptionGroupTestComponent {
selectDisabled = false;
optionGroupDisabled = false;
optionDisabled = false;
showOption = true;
optionGroupTitle = '';

@ViewChild(NbSelectComponent) selectComponent: NbSelectComponent<number>;
@ViewChild(NbOptionGroupComponent) optionGroupComponent: NbOptionGroupComponent;
@ViewChild(NbOptionComponent) optionComponent: NbOptionComponent<number>;
}

describe('NbOptionGroupComponent', () => {
let fixture: ComponentFixture<NbOptionGroupTestComponent>;
let testComponent: NbOptionGroupTestComponent;
let selectComponent: NbSelectComponent<number>;
let optionGroupComponent: NbOptionGroupComponent;
let optionComponent: NbOptionComponent<number>;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([]),
NbThemeModule.forRoot(),
NbLayoutModule,
NbSelectModule,
],
declarations: [ NbOptionGroupTestComponent ],
});

fixture = TestBed.createComponent(NbOptionGroupTestComponent);
testComponent = fixture.componentInstance;
fixture.detectChanges();
flush();

selectComponent = testComponent.selectComponent;
optionGroupComponent = testComponent.optionGroupComponent;
optionComponent = testComponent.optionComponent;
}));

it('should contain passed title', () => {
const title = 'random option group title';
selectComponent.show();
testComponent.optionGroupTitle = title;
fixture.detectChanges();

const groupTitle = fixture.debugElement.query(By.directive(NbOptionGroupComponent))
.query(By.css('.option-group-title'));

expect(groupTitle.nativeElement.textContent).toEqual(title);
});

it('should have disabled attribute if disabled', () => {
selectComponent.show();
testComponent.optionGroupDisabled = true;
fixture.detectChanges();

const optionGroup = fixture.debugElement.query(By.directive(NbOptionGroupComponent));
expect(optionGroup.attributes.disabled).toEqual('');
});

it('should remove disabled attribute if disabled set to false', () => {
selectComponent.show();
testComponent.optionGroupDisabled = true;
fixture.detectChanges();

testComponent.optionGroupDisabled = false;
fixture.detectChanges();

const optionGroup = fixture.debugElement.query(By.directive(NbOptionGroupComponent));
expect(optionGroup.attributes.disabled).toEqual(null);
});

it('should disable group options if group disabled', () => {
const setDisabledSpy = spyOn(optionComponent, 'setDisabledByGroupState');

optionGroupComponent.disabled = true;
fixture.detectChanges();

expect(setDisabledSpy).toHaveBeenCalledTimes(1);
expect(setDisabledSpy).toHaveBeenCalledWith(true);
});

it('should enable group options if group enabled', () => {
testComponent.optionDisabled = true;
fixture.detectChanges();

expect(optionComponent.disabled).toEqual(true);

const setDisabledSpy = spyOn(optionComponent, 'setDisabledByGroupState');
optionGroupComponent.disabled = false;

expect(setDisabledSpy).toHaveBeenCalledTimes(1);
expect(setDisabledSpy).toHaveBeenCalledWith(false);
});

it('should update options state when options change', fakeAsync(() => {
testComponent.optionGroupDisabled = true;
testComponent.showOption = false;
fixture.detectChanges();
flush();

testComponent.showOption = true;
fixture.detectChanges();
flush();
fixture.detectChanges();

expect(optionComponent.disabledAttribute).toEqual('');
}));

it('should update options state after content initialisation', fakeAsync(() => {
fixture = TestBed.createComponent(NbOptionGroupTestComponent);
testComponent = fixture.componentInstance;
testComponent.optionDisabled = true;
fixture.detectChanges();
flush();

expect(testComponent.optionComponent.disabledAttribute).toEqual('');
}));
});
56 changes: 53 additions & 3 deletions src/framework/theme/components/select/option-group.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { ChangeDetectionStrategy, Component, HostBinding, Input } from '@angular/core';
import { convertToBoolProperty } from '../helpers';
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ContentChildren,
HostBinding,
Input,
OnDestroy,
QueryList,
} from '@angular/core';
import { takeWhile } from 'rxjs/operators';

import { convertToBoolProperty } from '../helpers';
import { NbOptionComponent } from './option.component';

@Component({
selector: 'nb-option-group',
Expand All @@ -17,7 +28,10 @@ import { convertToBoolProperty } from '../helpers';
<ng-content select="nb-option, ng-container"></ng-content>
`,
})
export class NbOptionGroupComponent {
export class NbOptionGroupComponent implements AfterContentInit, OnDestroy {

protected alive = true;

@Input() title: string;

@Input()
Expand All @@ -26,13 +40,49 @@ export class NbOptionGroupComponent {
}
set disabled(value: boolean) {
this._disabled = convertToBoolProperty(value);

if (this.options) {
this.updateOptionsDisabledState();
}
}
protected _disabled: boolean = false;

@HostBinding('attr.disabled')
get disabledAttribute(): '' | null {
return this.disabled ? '' : null;
}

@ContentChildren(NbOptionComponent, { descendants: true }) options: QueryList<NbOptionComponent<any>>;

ngAfterContentInit() {
if (this.options.length) {
this.asyncUpdateOptionsDisabledState();
}

this.options.changes
.pipe(takeWhile(() => this.alive))
.subscribe(() => this.asyncUpdateOptionsDisabledState());
}

ngOnDestroy() {
this.alive = false;
}

/**
* Sets disabled state for each option to current group disabled state.
*/
protected updateOptionsDisabledState(): void {
this.options.forEach((option: NbOptionComponent<any>) => option.setDisabledByGroupState(this.disabled));
}

/**
* Updates options disabled state after promise resolution.
* This way change detection will be triggered after options state updated.
* Use this method when updating options during change detection run (e.g. QueryList.changes, lifecycle hooks).
*/
protected asyncUpdateOptionsDisabledState(): void {
Promise.resolve().then(() => this.updateOptionsDisabledState());
}
}


16 changes: 15 additions & 1 deletion src/framework/theme/components/select/option.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ import { NbSelectComponent } from './select.component';
`,
})
export class NbOptionComponent<T> implements OnDestroy {

protected disabledByGroup = false;

/**
* Option value that will be fired on selection.
* */
Expand Down Expand Up @@ -103,7 +106,8 @@ export class NbOptionComponent<T> implements OnDestroy {

@HostBinding('attr.disabled')
get disabledAttribute(): '' | null {
return this.disabled ? '' : null;
const disabled = this.disabledByGroup || this.disabled;
return disabled ? '' : null;
}

@HostListener('click')
Expand All @@ -119,6 +123,16 @@ export class NbOptionComponent<T> implements OnDestroy {
this.setSelection(false);
}

/**
* Sets disabled by group state and marks component for check.
*/
setDisabledByGroupState(disabled: boolean): void {
if (this.disabledByGroup !== disabled) {
this.disabledByGroup = disabled;
this.cd.markForCheck();
}
}

protected setSelection(selected: boolean): void {
/**
* In case of changing options in runtime the reference to the selected option will be kept in select component.
Expand Down
83 changes: 83 additions & 0 deletions src/framework/theme/components/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { NB_DOCUMENT } from '../../theme.options';
import { NbSelectComponent } from './select.component';
import { NbLayoutModule } from '../layout/layout.module';
import { NbOptionComponent } from './option.component';
import { NbOptionGroupComponent } from './option-group.component';


const TEST_GROUPS = [
Expand Down Expand Up @@ -248,6 +249,30 @@ export class NbSelectWithFalsyOptionValuesComponent {
})
export class NbMultipleSelectWithFalsyOptionValuesComponent extends NbSelectWithFalsyOptionValuesComponent {}

@Component({
template: `
<nb-layout>
<nb-layout-column>
<nb-select>
<nb-option-group [disabled]="optionGroupDisabled">
<nb-option [value]="1" [disabled]="optionDisabled">1</nb-option>
</nb-option-group>
</nb-select>
</nb-layout-column>
</nb-layout>
`,
})
export class NbOptionDisabledTestComponent {
optionGroupDisabled = false;
optionDisabled = false;

@ViewChild(NbSelectComponent) selectComponent: NbSelectComponent<number>;
@ViewChild(NbOptionGroupComponent) optionGroupComponent: NbOptionGroupComponent;
@ViewChild(NbOptionComponent) optionComponent: NbOptionComponent<number>;
}

describe('Component: NbSelectComponent', () => {
let fixture: ComponentFixture<NbSelectTestComponent>;
let overlayContainerService: NbOverlayContainerAdapter;
Expand Down Expand Up @@ -729,3 +754,61 @@ describe('NbOptionComponent', () => {
expect(selectionChangeSpy).toHaveBeenCalledTimes(1);
}));
});

describe('NbOptionComponent disabled', () => {
let fixture: ComponentFixture<NbOptionDisabledTestComponent>;
let testComponent: NbOptionDisabledTestComponent;
let selectComponent: NbSelectComponent<number>;
let optionGroupComponent: NbOptionGroupComponent;
let optionComponent: NbOptionComponent<number>;

beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
imports: [
RouterTestingModule.withRoutes([]),
NbThemeModule.forRoot(),
NbLayoutModule,
NbSelectModule,
],
declarations: [ NbOptionDisabledTestComponent ],
});

fixture = TestBed.createComponent(NbOptionDisabledTestComponent);
testComponent = fixture.componentInstance;
fixture.detectChanges();
flush();

selectComponent = testComponent.selectComponent;
optionGroupComponent = testComponent.optionGroupComponent;
optionComponent = testComponent.optionComponent;
}));

it('should has disabled attribute if disabled set to true', () => {
selectComponent.show();
testComponent.optionDisabled = true;
fixture.detectChanges();

const option = fixture.debugElement.query(By.directive(NbOptionComponent));
expect(option.attributes.disabled).toEqual('');
});

it('should has disabled attribute if group disabled set to true', fakeAsync(() => {
selectComponent.show();
testComponent.optionGroupDisabled = true;
fixture.detectChanges();
flush();
fixture.detectChanges();

const option = fixture.debugElement.query(By.directive(NbOptionComponent));
expect(option.attributes.disabled).toEqual('');
}));

it('should not change disabled property if group disabled changes', fakeAsync(() => {
testComponent.optionGroupDisabled = true;
fixture.detectChanges();
flush();
fixture.detectChanges();

expect(optionComponent.disabled).toEqual(false);
}));
});
3 changes: 3 additions & 0 deletions src/framework/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ export * from './components/toastr/toastr.service';
export * from './components/tooltip/tooltip.module';
export * from './components/tooltip/tooltip.directive';
export * from './components/select/select.module';
export * from './components/select/select.component';
export * from './components/select/option.component';
export * from './components/select/option-group.component';
export * from './components/window';
export * from './components/datepicker/datepicker.module';
export * from './components/datepicker/datepicker.directive';
Expand Down

0 comments on commit 54da610

Please sign in to comment.