Skip to content
This repository has been archived by the owner on Dec 8, 2022. It is now read-only.

Commit

Permalink
Added required input to radio group (#79)
Browse files Browse the repository at this point in the history
* Added input property for radio group

* code style cleanup

* tightening up the screws
  • Loading branch information
Blackbaud-AlexKingman committed Nov 19, 2019
1 parent 97cba26 commit fdd8e56
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 37 deletions.
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
<form [formGroup]="radioForm">
<form
[formGroup]="radioForm"
>
<sky-radio-group *ngIf="radioGroupEnabled"
class="form-group radio"
formControlName="option"
name="option"
[ariaLabel]="ariaLabel"
[ariaLabelledBy]="ariaLabelledBy"
[required]="required"
[tabIndex]="tabIndex"
>
<legend
id="radio-group-label">
id="radio-group-label"
>
Reactive Radio button options:
</legend>
<ul class="sky-list-unstyled">
<ul
class="sky-list-unstyled"
>
<li *ngFor="let value of options">
<sky-radio
[disabled]="value.disabled"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,11 @@ import {
templateUrl: './radio-group.component.fixture.html'
})
export class SkyRadioGroupTestComponent {
@ViewChild(SkyRadioGroupComponent)
public radioGroupComponent: SkyRadioGroupComponent;

public radioGroupEnabled = true;

public radioForm: FormGroup;

public tabIndex: number;
public ariaLabel: string;

public ariaLabelledBy: string = 'radio-group-label';

public ariaLabel: string;

public options = [
{ name: 'Lillith Corharvest', disabled: false },
{ name: 'Harima Kenji', disabled: false },
Expand All @@ -40,6 +32,17 @@ export class SkyRadioGroupTestComponent {

public radioControl = new FormControl();

public radioForm: FormGroup;

public radioGroupEnabled = true;

public required: boolean = false;

public tabIndex: number;

@ViewChild(SkyRadioGroupComponent)
public radioGroupComponent: SkyRadioGroupComponent;

constructor(
private fb: FormBuilder
) {
Expand Down
4 changes: 3 additions & 1 deletion src/app/public/modules/radio/radio-group.component.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<div
class="sky-radio-group"
role="radiogroup"
[attr.aria-labelledby]="ariaLabelledBy"
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledBy"
[attr.aria-required]="(required) ? true : null"
[attr.required]="(required) ? '' : null"
>
<ng-content></ng-content>
</div>
41 changes: 39 additions & 2 deletions src/app/public/modules/radio/radio-group.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,22 @@ describe('Radio group component', function () {
let fixture: ComponentFixture<SkyRadioGroupTestComponent>;
let componentInstance: SkyRadioGroupTestComponent;

//#region helpers
function getRadios(radioFixture: ComponentFixture<any>) {
return radioFixture.nativeElement.querySelectorAll('.sky-radio-input');
}

function getRadioGroup(radioFixture: ComponentFixture<any>): HTMLElement {
return radioFixture.nativeElement.querySelector('.sky-radio-group');
}

function clickCheckbox(radioFixture: ComponentFixture<any>, index: number) {
const radios = getRadios(radioFixture);
radios.item(index).click();
fixture.detectChanges();
tick();
}
//#endregion

beforeEach(function () {
TestBed.configureTestingModule({
Expand Down Expand Up @@ -138,6 +144,37 @@ describe('Radio group component', function () {
expect(componentInstance.radioForm.value.option.name).toBe('Harima Kenji');
}));

it('should not show a required state when not required', fakeAsync(() => {
fixture.detectChanges();
tick();

const radioGroupDiv = getRadioGroup(fixture);
expect(radioGroupDiv.getAttribute('required')).toBeNull();
expect(radioGroupDiv.getAttribute('aria-required')).toBeNull();
}));

it('should show a required state when required input is set to true', fakeAsync(() => {
componentInstance.required = true;

fixture.detectChanges();
tick();

const radioGroupDiv = getRadioGroup(fixture);
expect(radioGroupDiv.getAttribute('required')).not.toBeNull();
expect(radioGroupDiv.getAttribute('aria-required')).toBe('true');
}));

it('should update the ngModel properly when radio button is required and changed', fakeAsync(() => {
componentInstance.required = true;
fixture.detectChanges();

expect(componentInstance.radioForm.valid).toBe(false);

clickCheckbox(fixture, 1);

expect(componentInstance.radioForm.valid).toBe(true);
}));

it('should use tabIndex when specified', fakeAsync(function () {
componentInstance.tabIndex = 2;
fixture.detectChanges();
Expand Down Expand Up @@ -260,7 +297,7 @@ describe('Radio group component', function () {
fixture.detectChanges();
tick();

const radioGroupDiv = fixture.nativeElement.querySelector('.sky-radio-group');
const radioGroupDiv = getRadioGroup(fixture);
expect(radioGroupDiv.getAttribute('aria-labelledby')).toBe('radio-group-label');
}));

Expand All @@ -271,7 +308,7 @@ describe('Radio group component', function () {
fixture.detectChanges();
tick();

const radioGroupDiv = fixture.nativeElement.querySelector('.sky-radio-group');
const radioGroupDiv = getRadioGroup(fixture);
expect(radioGroupDiv.getAttribute('aria-label')).toBe('radio-group-label-manual');
}));

Expand Down
63 changes: 44 additions & 19 deletions src/app/public/modules/radio/radio-group.component.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
// #region imports
import {
AfterContentInit,
AfterViewInit,
ChangeDetectorRef,
Component,
ContentChildren,
forwardRef,
Input,
OnDestroy,
QueryList
Optional,
QueryList,
Self
} from '@angular/core';

import {
ControlValueAccessor,
NG_VALUE_ACCESSOR
NgControl
} from '@angular/forms';

import {
Subject
} from 'rxjs/Subject';

import {
SkyFormsUtility
} from '../shared/forms-utility';

import {
SkyRadioChange
} from './types';

import {
SkyRadioComponent
} from './radio.component';
// #endregion

let nextUniqueId = 0;

const SKY_RADIO_GROUP_CONTROL_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
// tslint:disable-next-line:no-forward-ref no-use-before-declare
useExisting: forwardRef(() => SkyRadioGroupComponent),
multi: true
};

@Component({
selector: 'sky-radio-group',
templateUrl: './radio-group.component.html',
providers: [
SKY_RADIO_GROUP_CONTROL_VALUE_ACCESSOR
]
templateUrl: './radio-group.component.html'
})
export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAccessor, OnDestroy {
export class SkyRadioGroupComponent implements AfterContentInit, AfterViewInit, OnDestroy {

@Input()
public ariaLabelledBy: string;

Expand All @@ -59,6 +54,15 @@ export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAcc
return this._name;
}

/**
* Indicates whether the input is required for form validation.
* When you set this property to `true`, the component adds `aria-required` and `required`
* attributes to the input element so that forms display an invalid state until the input element
* is complete. This property accepts a `boolean` value.
*/
@Input()
public required: boolean = false;

@Input()
public set value(value: any) {
const isNewValue = value !== this._value;
Expand Down Expand Up @@ -90,9 +94,20 @@ export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAcc
private ngUnsubscribe = new Subject();

private _name = `sky-radio-group-${nextUniqueId++}`;
private _value: any;

private _tabIndex: number;

private _value: any;

constructor(
private changeDetector: ChangeDetectorRef,
@Self() @Optional() private ngControl: NgControl
) {
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
}

public ngAfterContentInit(): void {
this.resetRadioButtons();

Expand All @@ -109,6 +124,16 @@ export class SkyRadioGroupComponent implements AfterContentInit, ControlValueAcc
});
}

public ngAfterViewInit(): void {
if (this.ngControl) {
// Backwards compatibility support for anyone still using Validators.Required.
this.required = this.required || SkyFormsUtility.hasRequiredValidation(this.ngControl);

// Avoid an ExpressionChangedAfterItHasBeenCheckedError.
this.changeDetector.detectChanges();
}
}

public watchForSelections() {
this.radios.forEach((radio) => {
radio.change
Expand Down
15 changes: 14 additions & 1 deletion src/app/visual/radio/radio-visual.component.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
<button
type="button"
class="sky-btn sky-btn-primary sky-test-label"
(click)="required = !required"
>
Toggle required
</button>

<h1>
Radio buttons (reactive)
</h1>
Expand All @@ -8,13 +16,15 @@ <h1>
>
<h3
id="favorite-id"
[ngClass]="{ 'sky-control-label-required': required }"
>
What is your favorite season?
</h3>
<div id="screenshot-radio">
<sky-radio-group
ariaLabelledBy="favorite-id"
formControlName="favoriteSeason"
[required]="required"
>
<ul
class="sky-list-unstyled"
Expand Down Expand Up @@ -59,11 +69,14 @@ <h1>
Radio buttons (template-driven)
</h1>

<h3>
<h3
[ngClass]="{ 'sky-control-label-required': required }"
>
What is your favorite season?
</h3>
<div>
<sky-radio-group
[required]="required"
[(ngModel)]="radioValue"
#foo="ngModel"
>
Expand Down
11 changes: 9 additions & 2 deletions src/app/visual/radio/radio-visual.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,26 @@ import {
templateUrl: './radio-visual.component.html'
})
export class RadioVisualComponent implements OnInit {
public selectedValue = '3';

public iconSelectedValue = '1';
public valueGuy = '2';

public radioForm: FormGroup;

public radioValue: any;

public required: boolean = false;

public seasons = [
{ name: 'Spring', disabled: false },
{ name: 'Summer', disabled: false },
{ name: 'Fall', disabled: true },
{ name: 'Winter', disabled: false }
];

public selectedValue = '3';

public valueGuy = '2';

constructor(
private formBuilder: FormBuilder
) { }
Expand Down

0 comments on commit fdd8e56

Please sign in to comment.