Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(option group): propagate disabled state to child options #1416

Merged
merged 7 commits into from
Apr 24, 2019
Merged
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
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) {
Tibing marked this conversation as resolved.
Show resolved Hide resolved
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))
Tibing marked this conversation as resolved.
Show resolved Hide resolved
.subscribe(() => this.asyncUpdateOptionsDisabledState());
}

ngOnDestroy() {
this.alive = false;
}

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

/**
* 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 {
Tibing marked this conversation as resolved.
Show resolved Hide resolved
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 {
Tibing marked this conversation as resolved.
Show resolved Hide resolved
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