diff --git a/src/app/playground-components.ts b/src/app/playground-components.ts index ec4a697804..d5e1598859 100644 --- a/src/app/playground-components.ts +++ b/src/app/playground-components.ts @@ -794,6 +794,18 @@ export const PLAYGROUND_COMPONENTS: ComponentLink[] = [ component: 'RadioShowcaseComponent', name: 'Radio Showcase', }, + { + path: 'radio-statuses.component', + link: '/radio/radio-statuses.component', + component: 'RadioStatusesComponent', + name: 'Radio Statuses', + }, + { + path: 'radio-disabled-group.component', + link: '/radio/radio-disabled-group.component', + component: 'RadioDisabledGroupComponent', + name: 'Radio Disabled Group', + }, ], }, { diff --git a/src/framework/theme/components/radio/_radio.component.theme.scss b/src/framework/theme/components/radio/_radio.component.theme.scss index 78fcb955f4..26da5f118f 100644 --- a/src/framework/theme/components/radio/_radio.component.theme.scss +++ b/src/framework/theme/components/radio/_radio.component.theme.scss @@ -4,80 +4,66 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -@mixin input-border-color($color) { - input:checked + .radio-indicator, - input:hover:not(:disabled) + .radio-indicator { - border-color: $color; - } -} - -@mixin nb-input-status-color($origin-border-color) { - @include input-border-color($origin-border-color); - &.success { - @include input-border-color(nb-theme(color-success)); - } - &.warning { - @include input-border-color(nb-theme(color-warning)); - } - &.danger { - @include input-border-color(nb-theme(color-danger)); - } -} - -@mixin nb-radio-check-mark($size, $color) { - &::before { - background-color: $color; - height: calc(#{$size} * 0.6); - width: calc(#{$size} * 0.6); - border: solid $color; - } -} - -@mixin set-box-style($bg, $size, $border-size, $border-color) { - background-color: $bg; - width: $size; - height: $size; - border: $border-size solid $border-color; -} - @mixin nb-radio-theme() { nb-radio { - .radio-indicator { - @include set-box-style( - nb-theme(radio-bg), - nb-theme(radio-size), - nb-theme(radio-border-size), - nb-theme(radio-border-color) - ); - - @include nb-radio-check-mark(nb-theme(radio-size), nb-theme(radio-checkmark)); + .radio-circle { + height: nb-theme(radio-height); + width: nb-theme(radio-width); + background-color: nb-theme(radio-background-color); + border-style: nb-theme(radio-border-style); + border-width: nb-theme(radio-border-width); } - input:checked + .radio-indicator, - input:disabled:checked + .radio-indicator { - @include set-box-style( - nb-theme(radio-checked-bg), - nb-theme(radio-checked-size), - nb-theme(radio-checked-border-size), - nb-theme(radio-checked-border-color) - ); - @include nb-radio-check-mark(nb-theme(radio-checked-size), nb-theme(radio-checked-checkmark)); + .native-input:focus + .radio-circle { + box-shadow: 0 0 0 nb-theme(radio-outline-width) nb-theme(radio-outline-color); } - input:disabled + .radio-indicator { - @include set-box-style( - nb-theme(radio-disabled-bg), - nb-theme(radio-disabled-size), - nb-theme(radio-disabled-border-size), - nb-theme(radio-disabled-border-color) - ); - @include nb-radio-check-mark(nb-theme(radio-disabled-size), nb-theme(radio-disabled-checkmark)); + .native-input:disabled + .radio-circle { + border-color: nb-theme(radio-disabled-border-color); + } + .native-input:disabled:checked + .radio-circle::before { + background-color: nb-theme(radio-disabled-inner-circle-color); } + .native-input:disabled ~ .text { + color: nb-theme(radio-disabled-text-color); + } + + @each $status in nb-get-statuses() { + &.status-#{$status} .native-input:enabled + .radio-circle { + border-color: nb-theme(radio-#{$status}-border-color); + } + &.status-#{$status} .native-input:enabled:checked + .radio-circle::before { + background-color: nb-theme(radio-#{$status}-inner-circle-color);; + } + + &.status-#{$status} .native-input:enabled:focus + .radio-circle { + border-color: nb-theme(radio-#{$status}-focus-border-color); + } + &.status-#{$status} .native-input:enabled:checked:focus + .radio-circle::before { + background-color: nb-theme(radio-#{$status}-focus-inner-circle-color); + } - @include nb-input-status-color(nb-theme(radio-checked-border-color)); + &.status-#{$status} label:hover .native-input:enabled + .radio-circle { + border-color: nb-theme(radio-#{$status}-hover-border-color); + } + &.status-#{$status} label:hover .native-input:checked:enabled + .radio-circle::before { + background-color: nb-theme(radio-#{$status}-hover-inner-circle-color); + } + + &.status-#{$status} .native-input:enabled:active + .radio-circle { + border-color: nb-theme(radio-#{$status}-active-border-color); + } + &.status-#{$status} .native-input:enabled:checked:active + .radio-circle::before { + background-color: nb-theme(radio-#{$status}-active-inner-circle-color); + } + } - .radio-description { - color: nb-theme(radio-fg); + .text { + color: nb-theme(radio-text-color); + font-family: nb-theme(radio-text-font-family); + font-size: nb-theme(radio-text-font-size); + font-weight: nb-theme(radio-text-font-weight); + line-height: nb-theme(radio-text-line-height); } } } diff --git a/src/framework/theme/components/radio/radio-group.component.ts b/src/framework/theme/components/radio/radio-group.component.ts index 3c017f0772..69be77aec1 100644 --- a/src/framework/theme/components/radio/radio-group.component.ts +++ b/src/framework/theme/components/radio/radio-group.component.ts @@ -7,7 +7,6 @@ import { AfterContentInit, ChangeDetectionStrategy, - ChangeDetectorRef, Component, ContentChildren, EventEmitter, @@ -23,11 +22,11 @@ import { import { isPlatformBrowser } from '@angular/common'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { fromEvent, merge } from 'rxjs'; -import { filter, switchMap, take, takeWhile } from 'rxjs/operators'; +import { filter, switchMap, take, takeUntil, takeWhile } from 'rxjs/operators'; import { convertToBoolProperty } from '../helpers'; import { NB_DOCUMENT } from '../../theme.options'; import { NbRadioComponent } from './radio.component'; - +import { NbComponentStatus } from '../component-status'; /** * The `NbRadioGroupComponent` is the wrapper for `nb-radio` button. @@ -35,9 +34,9 @@ import { NbRadioComponent } from './radio.component'; * * ```html * - * Option 1 - * Option 2 - * Option 3 + * Option 1 + * Option 2 + * Option 3 * * ``` * @@ -45,9 +44,9 @@ import { NbRadioComponent } from './radio.component'; * * ```html * - * Option 1 - * Option 2 - * Option 3 + * Option 1 + * Option 2 + * Option 3 * * ``` * @@ -59,13 +58,12 @@ import { NbRadioComponent } from './radio.component'; * * ``` * + * You can change radio group status by setting `status` input. + * @stacked-example(Statuses, radio/radio-statuses.component) + * * Also, you can disable the whole group using `disabled` attribute. + * @stacked-example(Disabled group, radio/radio-disabled-group.component) * - * ```html - * - * ... - * - * ``` * */ @Component({ selector: 'nb-radio-group', @@ -82,48 +80,86 @@ import { NbRadioComponent } from './radio.component'; }) export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, ControlValueAccessor { - @ContentChildren(NbRadioComponent, { descendants: true }) radios: QueryList; + protected alive: boolean = true; + protected isTouched: boolean = false; + protected onChange = (value: any) => {}; + protected onTouched = () => {}; - @Input('value') - set setValue(value: any) { - this.value = value; + @Input() + get value(): any { + return this._value; + } + set value(value: any) { + this._value = value; this.updateValues(); } + protected _value: any; - @Input('name') - set setName(name: string) { - this.name = name; + @Input() + get name(): string { + return this._name; + } + set name(name: string) { + this._name = name; this.updateNames(); } + protected _name: string; - @Input('disabled') - set setDisabled(disabled: boolean) { - this.disabled = convertToBoolProperty(disabled); + @Input() + get disabled(): boolean { + return this._disabled; + } + set disabled(disabled: boolean) { + this._disabled = convertToBoolProperty(disabled); this.updateDisabled(); } + protected _disabled: boolean; - @Output() valueChange: EventEmitter = new EventEmitter(); + /** + * Radio buttons status. + * Possible values are `primary` (default), `success`, `warning`, `danger`, `info`. + */ + @Input() + get status(): NbComponentStatus { + return this._status; + } + set status(value: NbComponentStatus) { + if (this._status !== value) { + this._status = value; + this.updateStatus(); + } + } + protected _status: NbComponentStatus = 'primary'; - protected disabled: boolean; - protected value: any; - protected name: string; - protected alive: boolean = true; - protected onChange = (value: any) => {}; - protected onTouched = () => {}; + @ContentChildren(NbRadioComponent, { descendants: true }) radios: QueryList; + + @Output() valueChange: EventEmitter = new EventEmitter(); constructor( - protected cd: ChangeDetectorRef, protected hostElement: ElementRef, @Inject(PLATFORM_ID) protected platformId, @Inject(NB_DOCUMENT) protected document, ) {} ngAfterContentInit() { + // In case option 'name' isn't set on nb-radio component, + // we need to set it's name right away, so it won't overlap with options + // without names from other radio groups. Otherwise they all would have + // same name and will be considered as options from one group so only the + // last option will stay selected. this.updateNames(); - this.updateValues(); - this.updateDisabled(); - this.subscribeOnRadiosValueChange(); - this.subscribeOnRadiosBlur(); + + Promise.resolve().then(() => this.updateAndSubscribeToRadios()); + + this.radios.changes + .pipe(takeWhile(() => this.alive)) + .subscribe(() => { + // 'changes' emit during change detection run and we can't update + // option properties right of since they already was initialized. + // Instead we schedule microtask to update radios after change detection + // run is finished. + Promise.resolve().then(() => this.updateAndSubscribeToRadios()); + }); } ngOnDestroy() { @@ -146,30 +182,43 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr } } + protected updateAndSubscribeToRadios() { + this.updateNames(); + this.updateValues(); + this.updateDisabled(); + this.updateStatus(); + this.subscribeOnRadiosValueChange(); + this.subscribeOnRadiosBlur(); + } + protected updateNames() { if (this.radios) { this.radios.forEach((radio: NbRadioComponent) => radio.name = this.name); - this.markRadiosForCheck(); } } protected updateValues() { if (this.radios && typeof this.value !== 'undefined') { this.radios.forEach((radio: NbRadioComponent) => radio.checked = radio.value === this.value); - this.markRadiosForCheck(); } } protected updateDisabled() { if (this.radios && typeof this.disabled !== 'undefined') { - this.radios.forEach((radio: NbRadioComponent) => radio.setDisabled = this.disabled); - this.markRadiosForCheck(); + this.radios.forEach((radio: NbRadioComponent) => radio.disabled = this.disabled); } } protected subscribeOnRadiosValueChange() { + if (!this.radios || !this.radios.length) { + return; + } + merge(...this.radios.map((radio: NbRadioComponent) => radio.valueChange)) - .pipe(takeWhile(() => this.alive)) + .pipe( + takeWhile(() => this.alive), + takeUntil(this.radios.changes), + ) .subscribe((value: any) => { this.writeValue(value); this.propagateValue(value); @@ -181,18 +230,16 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr this.onChange(value); } - protected markRadiosForCheck() { - this.radios.forEach((radio: NbRadioComponent) => radio.markForCheck()); - } - protected subscribeOnRadiosBlur() { - if (!isPlatformBrowser(this.platformId)) { + const hasNoRadios = !this.radios || !this.radios.length; + if (!isPlatformBrowser(this.platformId) || this.isTouched || hasNoRadios) { return; } const hostElement = this.hostElement.nativeElement; fromEvent(hostElement, 'focusin') .pipe( + takeWhile(() => this.alive), filter(event => hostElement.contains(event.target as Node)), switchMap(() => merge( fromEvent(this.document, 'focusin'), @@ -200,7 +247,19 @@ export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, Contr )), filter(event => !hostElement.contains(event.target as Node)), take(1), + takeUntil(this.radios.changes), ) - .subscribe(() => this.onTouched()); + .subscribe(() => this.markTouched()); + } + + protected markTouched() { + this.isTouched = true; + this.onTouched(); + } + + protected updateStatus() { + if (this.radios) { + this.radios.forEach((radio: NbRadioComponent) => radio.status = this.status); + } } } diff --git a/src/framework/theme/components/radio/radio.component.scss b/src/framework/theme/components/radio/radio.component.scss index afe7e64422..645cda9bcb 100644 --- a/src/framework/theme/components/radio/radio.component.scss +++ b/src/framework/theme/components/radio/radio.component.scss @@ -10,40 +10,30 @@ display: block; label { - position: relative; display: inline-flex; margin: 0; min-height: inherit; padding: 0.375rem 1.5rem 0.375rem 0; + align-items: center; } - input { - position: absolute; - opacity: 0; - - &:disabled { - & + .radio-indicator, - & ~ .radio-description { - opacity: 0.5; - } - } - } - - .radio-indicator { + .radio-circle { border-radius: 50%; + border-style: solid; flex-shrink: 0; display: flex; justify-content: center; align-items: center; + } - &::before { - content: ''; - transition: all 0.1s; - border-radius: 50%; - } + .radio-circle::before { + content: ''; + border-radius: 50%; + height: 75%; + width: 75%; } - .radio-description { + .text { @include nb-ltr(padding-left, 0.5rem); @include nb-rtl(padding-right, 0.5rem); } diff --git a/src/framework/theme/components/radio/radio.component.ts b/src/framework/theme/components/radio/radio.component.ts index a8875a5d83..ae644b5e76 100644 --- a/src/framework/theme/components/radio/radio.component.ts +++ b/src/framework/theme/components/radio/radio.component.ts @@ -4,10 +4,18 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; import { convertToBoolProperty } from '../helpers'; - +import { NbComponentStatus } from '../component-status'; /** * The `NbRadioComponent` provides the same functionality as native `` @@ -22,7 +30,7 @@ import { convertToBoolProperty } from '../helpers'; * ```ts * @NgModule({ * imports: [ - * // ... + * // ... * NbRadioModule, * ], * }) @@ -35,9 +43,9 @@ import { convertToBoolProperty } from '../helpers'; * * ```html * - * Option 1 - * Option 2 - * Option 3 + * Option 1 + * Option 2 + * Option 3 * * ``` * @@ -48,22 +56,61 @@ import { convertToBoolProperty } from '../helpers'; * * @styles * - * radio-bg - * radio-fg - * radio-size - * radio-border-size - * radio-border-color - * radio-checkmark - * radio-checked-bg - * radio-checked-size - * radio-checked-border-size - * radio-checked-border-color - * radio-checked-checkmark - * radio-disabled-bg - * radio-disabled-size - * radio-disabled-border-size - * radio-disabled-border-color - * radio-disabled-checkmark + * radio-width + * radio-height: + * radio-background-color: + * radio-border-style: + * radio-border-width: + * radio-text-color: + * radio-text-font-family: + * radio-text-font-size: + * radio-text-font-weight: + * radio-text-line-height: + * radio-outline-color: + * radio-outline-width: + * radio-disabled-border-color: + * radio-disabled-text-color: + * radio-disabled-inner-circle-color: + * radio-primary-border-color: + * radio-primary-inner-circle-color: + * radio-primary-focus-border-color: + * radio-primary-focus-inner-circle-color: + * radio-primary-hover-border-color: + * radio-primary-hover-inner-circle-color: + * radio-primary-active-border-color: + * radio-primary-active-inner-circle-color: + * radio-success-border-color: + * radio-success-inner-circle-color: + * radio-success-focus-border-color: + * radio-success-focus-inner-circle-color: + * radio-success-hover-border-color: + * radio-success-hover-inner-circle-color: + * radio-success-active-border-color: + * radio-success-active-inner-circle-color: + * radio-warning-border-color: + * radio-warning-inner-circle-color: + * radio-warning-focus-border-color: + * radio-warning-focus-inner-circle-color: + * radio-warning-hover-border-color: + * radio-warning-hover-inner-circle-color: + * radio-warning-active-border-color: + * radio-warning-active-inner-circle-color: + * radio-danger-border-color: + * radio-danger-inner-circle-color: + * radio-danger-focus-border-color: + * radio-danger-focus-inner-circle-color: + * radio-danger-hover-border-color: + * radio-danger-hover-inner-circle-color: + * radio-danger-active-border-color: + * radio-danger-active-inner-circle-color: + * radio-info-border-color: + * radio-info-inner-circle-color: + * radio-info-focus-border-color: + * radio-info-focus-inner-circle-color: + * radio-info-hover-border-color: + * radio-info-hover-inner-circle-color: + * radio-info-active-border-color: + * radio-info-active-inner-circle-color: * */ @Component({ selector: 'nb-radio', @@ -71,14 +118,15 @@ import { convertToBoolProperty } from '../helpers'; @@ -88,28 +136,97 @@ import { convertToBoolProperty } from '../helpers'; }) export class NbRadioComponent { - @Input() name: string; + @Input() + get name(): string { + return this._name; + } + set name(value: string) { + if (this._name !== value) { + this._name = value; + this.cd.detectChanges(); + } + } + private _name: string; + + @Input() + get checked(): boolean { + return this._checked; + } + set checked(value: boolean) { + const boolValue = convertToBoolProperty(value); + if (this._checked !== boolValue) { + this._checked = boolValue; + this.cd.markForCheck(); + } + } + private _checked: boolean = false; - @Input() checked: boolean; + @Input() + get value(): any { + return this._value; + } + set value(value: any) { + if (this._value !== value) { + this._value = value; + this.cd.markForCheck(); + } + } + private _value: any; - @Input() value: any; + @Input() + get disabled(): boolean { + return this._disabled; + } + set disabled(disabled: boolean) { + const boolValue = convertToBoolProperty(disabled); + if (this._disabled !== boolValue) { + this._disabled = boolValue; + this.cd.markForCheck(); + } + } + private _disabled: boolean = false; - @Input('disabled') - set setDisabled(disabled: boolean) { - this.disabled = convertToBoolProperty(disabled); + @Input() + get status(): NbComponentStatus { + return this._status; } + set status(value: NbComponentStatus) { + if (this._status !== value) { + this._status = value; + this.cd.markForCheck(); + } + } + private _status: NbComponentStatus = 'primary'; @Output() valueChange: EventEmitter = new EventEmitter(); @Output() blur: EventEmitter = new EventEmitter(); - disabled: boolean; - constructor(protected cd: ChangeDetectorRef) {} - markForCheck() { - this.cd.markForCheck(); - this.cd.detectChanges(); + @HostBinding('class.status-primary') + get isPrimary(): boolean { + return this.status === 'primary'; + } + + @HostBinding('class.status-success') + get isSuccess(): boolean { + return this.status === 'success'; + } + + @HostBinding('class.status-warning') + get isWarning(): boolean { + return this.status === 'warning'; + } + + @HostBinding('class.status-danger') + get isDanger(): boolean { + return this.status === 'danger'; + } + + @HostBinding('class.status-info') + get isInfo(): boolean { + return this.status === 'info'; } onChange(event: Event) { diff --git a/src/framework/theme/components/radio/radio.spec.ts b/src/framework/theme/components/radio/radio.spec.ts index 0ee3dbacb1..65e629a423 100644 --- a/src/framework/theme/components/radio/radio.spec.ts +++ b/src/framework/theme/components/radio/radio.spec.ts @@ -4,15 +4,26 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core/testing'; +import { + Component, + DebugElement, + ElementRef, + EventEmitter, + Input, + Output, + QueryList, + ViewChild, + ViewChildren, +} from '@angular/core'; +import { By } from '@angular/platform-browser'; +import createSpy = jasmine.createSpy; import { NbRadioModule } from './radio.module'; import { NbRadioComponent } from './radio.component'; +import { NbRadioGroupComponent } from './radio-group.component'; import { NB_DOCUMENT } from '../../theme.options'; -import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core'; -import { By } from '@angular/platform-browser'; - @Component({ selector: 'nb-radio-test', template: ` @@ -28,11 +39,44 @@ export class NbRadioTestComponent { @Output() valueChange = new EventEmitter(); } +@Component({ + template: ` + + + {{radio}} + + + `, +}) +export class NbRadioWithDynamicValuesTestComponent { + radioValues: number[] = []; + showRadios: boolean = false; + + @ViewChild(NbRadioGroupComponent) radioGroupComponent: NbRadioGroupComponent; + @ViewChildren(NbRadioComponent) radioComponents: QueryList; +} + +@Component({ + template: ` + + + + + + + `, +}) +export class NbTwoRadioGroupsComponent { + @ViewChild('firstGroup', { read: NbRadioGroupComponent }) firstGroup: NbRadioGroupComponent; + @ViewChild('secondGroup', { read: NbRadioGroupComponent }) secondGroup: NbRadioGroupComponent; + @ViewChildren(NbRadioComponent, { read: ElementRef }) radios: QueryList; +} + describe('radio', () => { let fixture: ComponentFixture; let comp: NbRadioTestComponent; - beforeEach(() => { + beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ imports: [NbRadioModule], declarations: [NbRadioTestComponent], @@ -42,7 +86,9 @@ describe('radio', () => { fixture = TestBed.createComponent(NbRadioTestComponent); comp = fixture.componentInstance; fixture.detectChanges(); - }); + flush(); + fixture.detectChanges(); + })); it('should render radios', () => { const radios: DebugElement[] = fixture.debugElement.queryAll(By.directive(NbRadioComponent)); @@ -56,3 +102,193 @@ describe('radio', () => { input.nativeElement.click(); }); }); + +describe('NbRadioGroupComponent', () => { + let fixture: ComponentFixture; + let radioTestComponent: NbRadioWithDynamicValuesTestComponent; + + beforeEach(fakeAsync(() => { + TestBed.configureTestingModule({ + imports: [ NbRadioModule ], + declarations: [ NbRadioWithDynamicValuesTestComponent, NbTwoRadioGroupsComponent ], + providers: [ { provide: NB_DOCUMENT, useValue: document } ], + }); + + fixture = TestBed.createComponent(NbRadioWithDynamicValuesTestComponent); + radioTestComponent = fixture.componentInstance; + fixture.detectChanges(); + flush(); // promise with 'updateAndSubscribeToRadios' + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + })); + + it('should update radio value when radios added after radio group initialization', fakeAsync(() => { + radioTestComponent.radioValues = [1, 2, 3]; + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.value = 1; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + expect(radioTestComponent.radioComponents.first.checked).toEqual(true); + const otherRadios = radioTestComponent.radioComponents.toArray().slice(1); + for (const radio of otherRadios) { + expect(radio.checked).toEqual(false); + } + })); + + it('should update radio name when radios added after radio group initialization', fakeAsync(() => { + const groupName = 'my-radio-group-name'; + radioTestComponent.radioValues = [1, 2, 3]; + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.name = groupName; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + for (const radio of radioTestComponent.radioComponents.toArray()) { + expect(radio.name).toEqual(groupName); + } + })); + + it('should update radio disabled state when radios added after radio group initialization', fakeAsync(() => { + radioTestComponent.radioValues = [1, 2, 3]; + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.disabled = true; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + for (const radio of radioTestComponent.radioComponents.toArray()) { + expect(radio.disabled).toEqual(true); + } + })); + + it('should update radio status when radios added after radio group initialization', fakeAsync(() => { + radioTestComponent.radioValues = [1, 2, 3]; + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.status = 'info'; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + for (const radio of radioTestComponent.radioComponents.toArray()) { + expect(radio.status).toEqual('info'); + } + })); + + it('should update subscription to radio change when radios added after radio group initialization', fakeAsync(() => { + const radioValue = 333; + radioTestComponent.radioValues = [radioValue]; + radioTestComponent.showRadios = true; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + const valueChangeSpy = createSpy('valueChange'); + radioTestComponent.radioGroupComponent.valueChange.subscribe(valueChangeSpy); + radioTestComponent.radioComponents.first.valueChange.emit(radioValue); + + tick(); + + expect(valueChangeSpy).toHaveBeenCalledTimes(1); + expect(valueChangeSpy).toHaveBeenCalledWith(radioValue); + })); + + it('should update radio value when radios change', fakeAsync(() => { + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.value = 1; + fixture.detectChanges(); + + radioTestComponent.radioValues = [1, 2, 3]; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + expect(radioTestComponent.radioComponents.first.checked).toEqual(true); + const otherRadios = radioTestComponent.radioComponents.toArray().slice(1); + for (const radio of otherRadios) { + expect(radio.checked).toBeFalsy(); + } + })); + + it('should update radio name when radios change', fakeAsync(() => { + const groupName = 'my-radio-group-name'; + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.name = groupName; + fixture.detectChanges(); + + radioTestComponent.radioValues = [1, 2, 3]; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + for (const radio of radioTestComponent.radioComponents.toArray()) { + expect(radio.name).toEqual(groupName); + } + })); + + it('should update radio disabled state when radios change', fakeAsync(() => { + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.disabled = true; + fixture.detectChanges(); + + radioTestComponent.radioValues = [1, 2, 3]; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + for (const radio of radioTestComponent.radioComponents.toArray()) { + expect(radio.disabled).toEqual(true); + } + })); + + it('should update radio status when radios change', fakeAsync(() => { + radioTestComponent.showRadios = true; + radioTestComponent.radioGroupComponent.status = 'info'; + fixture.detectChanges(); + + radioTestComponent.radioValues = [1, 2, 3]; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + for (const radio of radioTestComponent.radioComponents.toArray()) { + expect(radio.status).toEqual('info'); + } + })); + + it('should update subscription to radio change when radios change', fakeAsync(() => { + const valueChangeSpy = createSpy('valueChange'); + radioTestComponent.radioGroupComponent.valueChange.subscribe(valueChangeSpy); + radioTestComponent.showRadios = true; + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + + const radioValue = 333; + radioTestComponent.radioValues = [radioValue]; + fixture.detectChanges(); // adds radios + flush(); // promise with 'updateAndSubscribeToRadios' in NbRadioGroup.radios.changes + fixture.detectChanges(); // detect changes made during 'updateAndSubscribeToRadios' + + radioTestComponent.radioComponents.first.valueChange.emit(radioValue); + tick(); + + expect(valueChangeSpy).toHaveBeenCalledTimes(1); + expect(valueChangeSpy).toHaveBeenCalledWith(radioValue); + })); + + it(`should set options name right away so it won't overlap with options from another groups`, () => { + const radioFixture = TestBed.createComponent(NbTwoRadioGroupsComponent); + radioFixture.detectChanges(); + + const { firstGroup, secondGroup, radios } = radioFixture.componentInstance; + const radioFromFirstGroup = radios.first.nativeElement.querySelector('input'); + const radioFromSecondGroup = radios.last.nativeElement.querySelector('input'); + + expect(firstGroup.radios.first.checked).toEqual(true); + expect(radioFromFirstGroup.checked).toEqual(true); + expect(secondGroup.radios.first.checked).toEqual(true); + expect(radioFromSecondGroup.checked).toEqual(true); + }); +}); diff --git a/src/framework/theme/styles/themes/_default.scss b/src/framework/theme/styles/themes/_default.scss index cdf028888f..0fe7cdbca4 100644 --- a/src/framework/theme/styles/themes/_default.scss +++ b/src/framework/theme/styles/themes/_default.scss @@ -1008,22 +1008,67 @@ $theme: ( datepicker-shadow: none, datepicker-arrow-size: 11px, - radio-bg: transparent, - radio-fg: color-fg-text, - radio-size: 1.25rem, - radio-border-size: 2px, - radio-border-color: input-border-color, - radio-checkmark: transparent, - radio-checked-bg: transparent, - radio-checked-size: 1.25rem, - radio-checked-border-size: 2px, - radio-checked-border-color: color-success, - radio-checked-checkmark: color-success, - radio-disabled-bg: transparent, - radio-disabled-size: 1.25rem, - radio-disabled-border-size: 2px, - radio-disabled-border-color: radio-border-color, - radio-disabled-checkmark: radio-checkmark, + radio-width: 1.125rem, + radio-height: 1.125rem, + radio-background-color: transparent, + radio-border-style: solid, + radio-border-width: 0.0625rem, + radio-text-color: text-dark-color, + radio-text-font-family: text-subtitle-font-family, + radio-text-font-size: text-subtitle-font-size, + radio-text-font-weight: text-subtitle-font-weight, + radio-text-line-height: text-subtitle-line-height, + radio-outline-color: outline-color, + radio-outline-width: outline-width, + + radio-disabled-border-color: color-basic, + radio-disabled-text-color: text-disabled-color, + radio-disabled-inner-circle-color: color-basic, + + radio-primary-border-color: color-primary, + radio-primary-inner-circle-color: color-primary, + radio-primary-focus-border-color: color-primary-focus, + radio-primary-focus-inner-circle-color: color-primary-focus, + radio-primary-hover-border-color: color-primary-hover, + radio-primary-hover-inner-circle-color: color-primary-hover, + radio-primary-active-border-color: color-primary-active, + radio-primary-active-inner-circle-color: color-primary-active, + + radio-success-border-color: color-success, + radio-success-inner-circle-color: color-success, + radio-success-focus-border-color: color-success-focus, + radio-success-focus-inner-circle-color: color-success-focus, + radio-success-hover-border-color: color-success-hover, + radio-success-hover-inner-circle-color: color-success-hover, + radio-success-active-border-color: color-success-active, + radio-success-active-inner-circle-color: color-success-active, + + radio-warning-border-color: color-warning, + radio-warning-inner-circle-color: color-warning, + radio-warning-focus-border-color: color-warning-focus, + radio-warning-focus-inner-circle-color: color-warning-focus, + radio-warning-hover-border-color: color-warning-hover, + radio-warning-hover-inner-circle-color: color-warning-hover, + radio-warning-active-border-color: color-warning-active, + radio-warning-active-inner-circle-color: color-warning-active, + + radio-danger-border-color: color-danger, + radio-danger-inner-circle-color: color-danger, + radio-danger-focus-border-color: color-danger-focus, + radio-danger-focus-inner-circle-color: color-danger-focus, + radio-danger-hover-border-color: color-danger-hover, + radio-danger-hover-inner-circle-color: color-danger-hover, + radio-danger-active-border-color: color-danger-active, + radio-danger-active-inner-circle-color: color-danger-active, + + radio-info-border-color: color-info, + radio-info-inner-circle-color: color-info, + radio-info-focus-border-color: color-info-focus, + radio-info-focus-inner-circle-color: color-info-focus, + radio-info-hover-border-color: color-info-hover, + radio-info-hover-inner-circle-color: color-info-hover, + radio-info-active-border-color: color-info-active, + radio-info-active-inner-circle-color: color-info-active, tree-grid-cell-border-width: 1px, tree-grid-cell-border-style: solid, diff --git a/src/playground/with-layout/radio/radio-disabled-group.component.ts b/src/playground/with-layout/radio/radio-disabled-group.component.ts new file mode 100644 index 0000000000..77454b5ab8 --- /dev/null +++ b/src/playground/with-layout/radio/radio-disabled-group.component.ts @@ -0,0 +1,24 @@ +import { Component } from '@angular/core'; + +@Component({ + template: ` + + + + + {{ option.label }} + + + + + `, +}) +export class RadioDisabledGroupComponent { + options = [ + { value: 'This is value 1', label: 'Option 1' }, + { value: 'This is value 2', label: 'Option 2' }, + { value: 'This is value 3', label: 'Option 3' }, + { value: 'This is value 4', label: 'Option 4' }, + { value: 'This is value 5', label: 'Option 5' }, + ]; +} diff --git a/src/playground/with-layout/radio/radio-routing.module.ts b/src/playground/with-layout/radio/radio-routing.module.ts index da68a8ee04..443cc7e4d4 100644 --- a/src/playground/with-layout/radio/radio-routing.module.ts +++ b/src/playground/with-layout/radio/radio-routing.module.ts @@ -8,6 +8,8 @@ import { NgModule } from '@angular/core'; import { RouterModule, Route} from '@angular/router'; import { RadioDisabledComponent } from './radio-disabled.component'; import { RadioShowcaseComponent } from './radio-showcase.component'; +import { RadioStatusesComponent } from './radio-statuses.component'; +import { RadioDisabledGroupComponent } from './radio-disabled-group.component'; const routes: Route[] = [ { @@ -18,6 +20,14 @@ const routes: Route[] = [ path: 'radio-showcase.component', component: RadioShowcaseComponent, }, + { + path: 'radio-statuses.component', + component: RadioStatusesComponent, + }, + { + path: 'radio-disabled-group.component', + component: RadioDisabledGroupComponent, + }, ]; @NgModule({ diff --git a/src/playground/with-layout/radio/radio-statuses-group.component.scss b/src/playground/with-layout/radio/radio-statuses-group.component.scss new file mode 100644 index 0000000000..60622baa42 --- /dev/null +++ b/src/playground/with-layout/radio/radio-statuses-group.component.scss @@ -0,0 +1,8 @@ +:host { + display: flex; + flex-wrap: wrap; +} + +nb-radio-group { + padding: 1rem; +} diff --git a/src/playground/with-layout/radio/radio-statuses.component.ts b/src/playground/with-layout/radio/radio-statuses.component.ts new file mode 100644 index 0000000000..fbdfe2697c --- /dev/null +++ b/src/playground/with-layout/radio/radio-statuses.component.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; + +@Component({ + template: ` + + + {{ option.label }} + + + `, + styleUrls: ['./radio-statuses-group.component.scss'], +}) +export class RadioStatusesComponent { + options = [ + { value: 'This is value 1', label: 'Option 1', checked: true }, + { value: 'This is value 2', label: 'Option 2' }, + { value: 'This is value 3', label: 'Option 3' }, + { value: 'This is value 4', label: 'Option 4', disabled: true }, + ]; + + statuses = ['primary', 'success', 'warning', 'danger', 'info']; +} diff --git a/src/playground/with-layout/radio/radio.module.ts b/src/playground/with-layout/radio/radio.module.ts index b64b27c19b..ef0cde6c92 100644 --- a/src/playground/with-layout/radio/radio.module.ts +++ b/src/playground/with-layout/radio/radio.module.ts @@ -11,11 +11,16 @@ import { NbCardModule, NbRadioModule } from '@nebular/theme'; import { RadioRoutingModule } from './radio-routing.module'; import { RadioDisabledComponent } from './radio-disabled.component'; import { RadioShowcaseComponent } from './radio-showcase.component'; +import { RadioStatusesComponent } from './radio-statuses.component'; +import { RadioDisabledGroupComponent } from './radio-disabled-group.component'; @NgModule({ declarations: [ RadioDisabledComponent, RadioShowcaseComponent, + RadioDisabledComponent, + RadioStatusesComponent, + RadioDisabledGroupComponent, ], imports: [ CommonModule,