diff --git a/docs/structure.ts b/docs/structure.ts
index 1cb4438e0f..2925e8d08b 100644
--- a/docs/structure.ts
+++ b/docs/structure.ts
@@ -293,6 +293,15 @@ export const structure = [
'NbCheckboxComponent',
],
},
+ {
+ type: 'tabs',
+ name: 'Radio',
+ icon: 'radio.svg',
+ source: [
+ 'NbRadioComponent',
+ 'NbRadioGroupComponent',
+ ],
+ },
{
type: 'tabs',
name: 'Select',
diff --git a/src/framework/theme/components/radio/_radio.component.theme.scss b/src/framework/theme/components/radio/_radio.component.theme.scss
new file mode 100644
index 0000000000..9268bebb96
--- /dev/null
+++ b/src/framework/theme/components/radio/_radio.component.theme.scss
@@ -0,0 +1,163 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * 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($size, $color) {
+ &::before {
+ content: '';
+ position: absolute;
+ background-color: $color;
+ height: calc(#{$size} * 0.6);
+ width: calc(#{$size} * 0.6);
+ border: solid $color;
+ border-radius: 50%;
+ }
+}
+
+@mixin set-style($bg, $size, $border-size, $border-color) {
+ background-color: $bg;
+ width: $size;
+ height: $size;
+ border: $border-size solid $border-color;
+}
+
+@mixin description-style {
+ color: nb-theme(radio-fg);
+ padding-left: 0.25rem;
+}
+
+@mixin nb-radio-theme() {
+ nb-radio {
+ display: block;
+
+ label {
+ position: relative;
+ display: inline-flex;
+ margin: 0;
+ min-height: inherit;
+ padding: 0.375rem 1.5rem;
+ }
+
+ .radio-indicator {
+ @include set-style(
+ nb-theme(radio-bg),
+ nb-theme(radio-size),
+ nb-theme(radio-border-size),
+ nb-theme(radio-border-color)
+ );
+ border-radius: 50%;
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ left: 0;
+ flex: none;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ @include nb-radio(nb-theme(radio-size), nb-theme(radio-checkmark));
+
+ &::before {
+ width: 0;
+ height: 0;
+ transition: all 0.1s;
+ }
+ }
+
+ input {
+ position: absolute;
+ opacity: 0;
+ z-index: -1;
+
+ &:checked ~ .radio-indicator {
+ @include set-style(
+ nb-theme(radio-checked-bg),
+ nb-theme(radio-checked-size),
+ nb-theme(radio-checked-border-size),
+ nb-theme(radio-checked-border-color)
+ );
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include nb-radio(
+ nb-theme(radio-checked-size),
+ nb-theme(radio-checked-checkmark)
+ );
+ }
+
+ &:disabled ~ {
+ .radio-indicator {
+ @include set-style(
+ nb-theme(radio-disabled-bg),
+ nb-theme(radio-disabled-size),
+ nb-theme(radio-disabled-border-size),
+ nb-theme(radio-border-color)
+ );
+ opacity: 0.5;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include nb-radio(
+ nb-theme(radio-disabled-size),
+ nb-theme(radio-checkmark)
+ );
+ }
+
+ .radio-description {
+ opacity: 0.5;
+ @include description-style;
+ }
+ }
+
+ &:disabled:checked ~ {
+ .radio-indicator {
+ @include set-style(
+ nb-theme(radio-disabled-bg),
+ nb-theme(radio-disabled-size),
+ nb-theme(radio-disabled-border-size),
+ nb-theme(radio-checked-border-color)
+ );
+ opacity: 0.5;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ @include nb-radio(
+ nb-theme(radio-disabled-size),
+ nb-theme(radio-disabled-checkmark)
+ );
+ }
+ .radio-description {
+ opacity: 0.5;
+ @include description-style;
+ }
+ }
+ }
+
+ @include nb-input-status-color(nb-theme(radio-checked-border-color));
+
+ .radio-description {
+ @include description-style;
+ }
+ }
+}
diff --git a/src/framework/theme/components/radio/radio-group.component.ts b/src/framework/theme/components/radio/radio-group.component.ts
new file mode 100644
index 0000000000..13899309ae
--- /dev/null
+++ b/src/framework/theme/components/radio/radio-group.component.ts
@@ -0,0 +1,174 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import {
+ AfterContentInit,
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ ContentChildren,
+ EventEmitter,
+ forwardRef,
+ Input,
+ OnDestroy,
+ Output,
+ QueryList,
+} from '@angular/core';
+import { NbRadioComponent } from './radio.component';
+import { merge } from 'rxjs';
+import { takeWhile } from 'rxjs/operators';
+import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
+import { convertToBoolProperty } from '../helpers';
+
+
+/**
+ * The `NbRadioGroupComponent` is the wrapper for `nb-radio` button.
+ * It provides form bindings:
+ *
+ * ```html
+ *
+ * Option 1
+ * Option 2
+ * Option 3
+ *
+ * ```
+ *
+ * Also, you can use `value` and `valueChange` for binding without forms.
+ *
+ * ```html
+ *
+ * Option 1
+ * Option 2
+ * Option 3
+ *
+ * ```
+ *
+ * Radio items name has to be provided through `name` input property of the radio group.
+ *
+ * ```html
+ *
+ * ...
+ *
+ * ```
+ *
+ * Also, you can disable the whole group using `disabled` attribute.
+ *
+ * ```html
+ *
+ * ...
+ *
+ * ```
+ * */
+@Component({
+ selector: 'nb-radio-group',
+ template: `
+ `,
+ providers: [
+ {
+ provide: NG_VALUE_ACCESSOR,
+ useExisting: forwardRef(() => NbRadioGroupComponent),
+ multi: true,
+ },
+ ],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, ControlValueAccessor {
+
+ @ContentChildren(NbRadioComponent, { descendants: true }) radios: QueryList;
+
+ @Input('value')
+ set setValue(value: any) {
+ this.value = value;
+ this.updateValues();
+ }
+
+ @Input('name')
+ set setName(name: string){
+ this.name = name;
+ this.updateNames();
+ }
+
+ @Input('disabled')
+ set setDisabled(disabled: boolean) {
+ this.disabled = convertToBoolProperty(disabled);
+ this.updateDisabled();
+ }
+
+ @Output() valueChange: EventEmitter = new EventEmitter();
+
+ protected disabled: boolean;
+ protected value: any;
+ protected name: string;
+ protected alive: boolean = true;
+ protected onChange = (value: any) => {};
+
+ constructor(protected cd: ChangeDetectorRef) {}
+
+ ngAfterContentInit() {
+ this.updateNames();
+ this.updateValues();
+ this.updateDisabled();
+ this.subscribeOnRadiosValueChange();
+ }
+
+ ngOnDestroy() {
+ this.alive = false;
+ }
+
+ registerOnChange(fn: any): void {
+ this.onChange = fn;
+ }
+
+ registerOnTouched(fn: any): void {
+ }
+
+ writeValue(value: any): void {
+ this.value = value;
+ }
+
+ protected updateNames() {
+ if (this.radios) {
+ this.radios.forEach((radio: NbRadioComponent) => radio.name = this.name);
+ this.markRadiosForCheck();
+ }
+ }
+
+ protected updateValues() {
+ if (this.radios) {
+ this.radios.forEach((radio: NbRadioComponent) => radio.checked = radio.value === this.value);
+ this.markRadiosForCheck();
+ }
+ }
+
+ protected updateDisabled() {
+ if (this.radios) {
+ this.radios.forEach((radio: NbRadioComponent) => {
+ if (!radio.hasOwnProperty('disabled')) {
+ radio.setDisabled = this.disabled
+ }
+ });
+ this.markRadiosForCheck();
+ }
+ }
+
+ protected subscribeOnRadiosValueChange() {
+ merge(...this.radios.map((radio: NbRadioComponent) => radio.valueChange))
+ .pipe(takeWhile(() => this.alive))
+ .subscribe((value: any) => {
+ this.writeValue(value);
+ this.propagateValue(value);
+ });
+ }
+
+ protected propagateValue(value: any) {
+ this.valueChange.emit(value);
+ this.onChange(value);
+ }
+
+ protected markRadiosForCheck() {
+ this.radios.forEach((radio: NbRadioComponent) => radio.markForCheck());
+ }
+}
diff --git a/src/framework/theme/components/radio/radio.component.ts b/src/framework/theme/components/radio/radio.component.ts
new file mode 100644
index 0000000000..e36d894f15
--- /dev/null
+++ b/src/framework/theme/components/radio/radio.component.ts
@@ -0,0 +1,120 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * 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 { convertToBoolProperty } from '../helpers';
+
+
+/**
+ * The `NbRadioComponent` provides the same functionality as native ``
+ * with Nebular styles and animations.
+ *
+ * @stacked-example(Showcase, radio/radio-showcase.component)
+ *
+ * ### Installation
+ *
+ * Import `NbRadioModule` to your feature module.
+ *
+ * ```ts
+ * @NgModule({
+ * imports: [
+ * // ...
+ * NbRadioModule,
+ * ],
+ * })
+ * export class PageModule { }
+ * ```
+ *
+ * ### Usage
+ *
+ * Radio buttons should be wrapped in `nb-radio-group` to provide form bindings.
+ *
+ * ```html
+ *
+ * Option 1
+ * Option 2
+ * Option 3
+ *
+ * ```
+ *
+ * You can disable some radios in the group using a `disabled` attribute.
+ *
+ * @stacked-example(Disabled, radio/radio-disabled.component)
+ *
+ *
+ * @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
+ * */
+@Component({
+ selector: 'nb-radio',
+ template: `
+
+ `,
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class NbRadioComponent {
+ @Input() name: string;
+
+ @Input() checked: boolean;
+
+ @Input() value: any;
+
+ @Input('disabled')
+ set setDisabled(disabled: boolean) {
+ this.disabled = convertToBoolProperty(disabled);
+ }
+
+ @Output() valueChange: EventEmitter = new EventEmitter();
+
+ disabled: boolean;
+
+ constructor(protected cd: ChangeDetectorRef) {}
+
+ markForCheck() {
+ this.cd.markForCheck();
+ this.cd.detectChanges();
+ }
+
+ onChange(event: Event) {
+ event.stopPropagation();
+ this.checked = true;
+ this.valueChange.emit(this.value);
+ }
+
+ onClick(event: Event) {
+ event.stopPropagation();
+ }
+}
diff --git a/src/framework/theme/components/radio/radio.module.ts b/src/framework/theme/components/radio/radio.module.ts
new file mode 100644
index 0000000000..372068fbce
--- /dev/null
+++ b/src/framework/theme/components/radio/radio.module.ts
@@ -0,0 +1,19 @@
+/*
+ * @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 { NbRadioComponent } from './radio.component';
+import { NbRadioGroupComponent } from './radio-group.component';
+
+
+@NgModule({
+ imports: [],
+ exports: [NbRadioComponent, NbRadioGroupComponent],
+ declarations: [NbRadioComponent, NbRadioGroupComponent],
+})
+export class NbRadioModule {
+}
diff --git a/src/framework/theme/components/radio/radio.spec.ts b/src/framework/theme/components/radio/radio.spec.ts
new file mode 100644
index 0000000000..cf47708060
--- /dev/null
+++ b/src/framework/theme/components/radio/radio.spec.ts
@@ -0,0 +1,56 @@
+/*
+ * @license
+ * Copyright Akveo. All Rights Reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ */
+
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NbRadioModule } from './radio.module';
+import { NbRadioComponent } from './radio.component';
+
+import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
+import { By } from '@angular/platform-browser';
+
+@Component({
+ selector: 'nb-radio-test',
+ template: `
+
+ 1
+ 2
+ 3
+
+ `,
+})
+export class NbRadioTestComponent {
+ @Input() value;
+ @Output() valueChange = new EventEmitter();
+}
+
+describe('radio', () => {
+ let fixture: ComponentFixture;
+ let comp: NbRadioTestComponent;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [NbRadioModule],
+ declarations: [NbRadioTestComponent],
+ });
+
+ fixture = TestBed.createComponent(NbRadioTestComponent);
+ comp = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should render radios', () => {
+ const radios: DebugElement[] = fixture.debugElement.queryAll(By.directive(NbRadioComponent));
+ expect(radios.length).toBe(3);
+ });
+
+ it('should fire value when selected', done => {
+ const secondRadio: DebugElement = fixture.debugElement.queryAll(By.directive(NbRadioComponent))[1];
+ comp.valueChange.subscribe(done);
+ const input = secondRadio.query(By.css('input'));
+ input.nativeElement.click();
+ });
+});
diff --git a/src/framework/theme/index.ts b/src/framework/theme/index.ts
index 00a96b53f6..4635962b4b 100644
--- a/src/framework/theme/index.ts
+++ b/src/framework/theme/index.ts
@@ -80,3 +80,4 @@ export * from './components/select/select.module';
export * from './components/window';
export * from './components/datepicker/datepicker.module';
export * from './components/datepicker/datepicker.directive';
+export * from './components/radio/radio.module';
diff --git a/src/framework/theme/styles/global/_components.scss b/src/framework/theme/styles/global/_components.scss
index 0104a3857b..c4530a2caa 100644
--- a/src/framework/theme/styles/global/_components.scss
+++ b/src/framework/theme/styles/global/_components.scss
@@ -36,6 +36,7 @@
@import '../../components/tooltip/tooltip.component.theme';
@import '../../components/window/window.component.theme';
@import '../../components/datepicker/datepicker-container.component.theme';
+@import '../../components/radio/radio.component.theme';
@mixin nb-theme-components() {
@@ -71,4 +72,5 @@
@include nb-tooltip-theme();
@include nb-window-theme();
@include nb-datepicker-theme();
+ @include nb-radio-theme();
}
diff --git a/src/framework/theme/styles/themes/_cosmic.scss b/src/framework/theme/styles/themes/_cosmic.scss
index 1182c2e201..8c8a664039 100644
--- a/src/framework/theme/styles/themes/_cosmic.scss
+++ b/src/framework/theme/styles/themes/_cosmic.scss
@@ -171,6 +171,9 @@ $theme: (
datepicker-border: color-primary,
datepicker-shadow: shadow,
+
+ radio-checked-border-color: color-primary,
+ radio-checked-checkmark: color-primary,
);
// register the theme
diff --git a/src/framework/theme/styles/themes/_default.scss b/src/framework/theme/styles/themes/_default.scss
index 9227bdaa52..eb526ecd5f 100644
--- a/src/framework/theme/styles/themes/_default.scss
+++ b/src/framework/theme/styles/themes/_default.scss
@@ -426,8 +426,6 @@ $theme: (
checkbox-disabled-border-color: color-fg-heading,
checkbox-disabled-checkmark: color-fg-heading,
- radio-fg: color-success,
-
modal-font-size: font-size,
modal-line-height: line-height,
modal-font-weight: font-weight-normal,
@@ -662,6 +660,23 @@ $theme: (
datepicker-border-radius: radius,
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: form-control-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: color-fg-heading,
+ radio-disabled-checkmark: color-fg-heading,
);
// register the theme
diff --git a/src/playground/playground-routing.module.ts b/src/playground/playground-routing.module.ts
index 46f3ed7fe7..baa6dce4ef 100644
--- a/src/playground/playground-routing.module.ts
+++ b/src/playground/playground-routing.module.ts
@@ -199,6 +199,8 @@ import { NbDatepickerShowcaseComponent } from './datepicker/datepicker-showcase.
import { NbDatepickerFormsComponent } from './datepicker/datepicker-forms.component';
import { NbDatepickerValidationComponent } from './datepicker/datepicker-validation.component';
import { NbRangepickerShowcaseComponent } from './datepicker/rangepicker-showcase.component';
+import { NbRadioShowcaseComponent } from './radio/radio-showcase.component';
+import { NbRadioDisabledComponent } from './radio/radio-disabled.component';
export const routes: Routes = [
@@ -313,6 +315,19 @@ export const routes: Routes = [
},
],
},
+ {
+ path: 'radio',
+ children: [
+ {
+ path: 'radio-showcase.component',
+ component: NbRadioShowcaseComponent,
+ },
+ {
+ path: 'radio-disabled.component',
+ component: NbRadioDisabledComponent,
+ },
+ ],
+ },
{
path: 'button',
children: [
diff --git a/src/playground/playground.module.ts b/src/playground/playground.module.ts
index 012394f463..40ee60bfc7 100644
--- a/src/playground/playground.module.ts
+++ b/src/playground/playground.module.ts
@@ -41,7 +41,7 @@ import {
NbDialogModule,
NbSelectModule,
NbWindowModule,
- NbDatepickerModule,
+ NbDatepickerModule, NbRadioModule,
} from '@nebular/theme';
import { NbPlaygroundRoutingModule } from './playground-routing.module';
@@ -248,6 +248,8 @@ import { NbDatepickerShowcaseComponent } from './datepicker/datepicker-showcase.
import { NbDatepickerFormsComponent } from './datepicker/datepicker-forms.component';
import { NbDatepickerValidationComponent } from './datepicker/datepicker-validation.component';
import { NbRangepickerShowcaseComponent } from './datepicker/rangepicker-showcase.component';
+import { NbRadioShowcaseComponent } from './radio/radio-showcase.component';
+import { NbRadioDisabledComponent } from './radio/radio-disabled.component';
export const NB_MODULES = [
NbCardModule,
@@ -288,6 +290,7 @@ export const NB_MODULES = [
NbSelectModule,
NbWindowModule.forRoot(),
NbDatepickerModule,
+ NbRadioModule,
];
export const NB_EXAMPLE_COMPONENTS = [
@@ -488,6 +491,8 @@ export const NB_EXAMPLE_COMPONENTS = [
NbDatepickerFormsComponent,
NbDatepickerValidationComponent,
NbRangepickerShowcaseComponent,
+ NbRadioShowcaseComponent,
+ NbRadioDisabledComponent,
];
@NgModule({
diff --git a/src/playground/radio/radio-disabled.component.html b/src/playground/radio/radio-disabled.component.html
new file mode 100644
index 0000000000..f9dd1a721d
--- /dev/null
+++ b/src/playground/radio/radio-disabled.component.html
@@ -0,0 +1,15 @@
+
+ Selected Option: {{ option }}
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
diff --git a/src/playground/radio/radio-disabled.component.ts b/src/playground/radio/radio-disabled.component.ts
new file mode 100644
index 0000000000..7e99892f83
--- /dev/null
+++ b/src/playground/radio/radio-disabled.component.ts
@@ -0,0 +1,22 @@
+/*
+ * @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-radio-disabled',
+ templateUrl: './radio-disabled.component.html',
+})
+export class NbRadioDisabledComponent {
+ options = [
+ { value: 'This is value 1', label: 'Option 1' },
+ { value: 'This is value 2', label: 'Option 2', disabled: true },
+ { value: 'This is value 3', label: 'Option 3' },
+ { value: 'This is value 4', label: 'Option 4', disabled: true },
+ { value: 'This is value 5', label: 'Option 5' },
+ ];
+ option;
+}
diff --git a/src/playground/radio/radio-showcase.component.html b/src/playground/radio/radio-showcase.component.html
new file mode 100644
index 0000000000..1ab65f71ef
--- /dev/null
+++ b/src/playground/radio/radio-showcase.component.html
@@ -0,0 +1,14 @@
+
+ Selected Option: {{ option }}
+
+
+
+
+ {{ option.label }}
+
+
+
+
+
diff --git a/src/playground/radio/radio-showcase.component.ts b/src/playground/radio/radio-showcase.component.ts
new file mode 100644
index 0000000000..09e6735a23
--- /dev/null
+++ b/src/playground/radio/radio-showcase.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-radio-showcase',
+ templateUrl: './radio-showcase.component.html',
+})
+export class NbRadioShowcaseComponent {
+ 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' },
+ ];
+ option;
+}