Skip to content
This repository has been archived by the owner on Feb 2, 2019. It is now read-only.

Commit

Permalink
feat(radio): support keyboard input behaviors and tests
Browse files Browse the repository at this point in the history
  - covers all radio group behaviors and keyboard inputs
  - add utility function for parsing tabindex attributes to reduce duplication.
  • Loading branch information
justindujardin committed Jan 3, 2016
1 parent d10288d commit 3aa8e0c
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 39 deletions.
3 changes: 2 additions & 1 deletion ng2-material/components/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {KeyCodes} from '../../core/key_codes';
import {KeyboardEvent} from 'angular2/src/facade/browser';
import {NumberWrapper} from 'angular2/src/facade/lang';
import {Input, Output, EventEmitter} from 'angular2/core';
import {parseTabIndexAttribute} from "../../core/util/util";

// TODO(jdd): ng-true-value, ng-false-value

Expand Down Expand Up @@ -44,7 +45,7 @@ export class MdCheckbox {

constructor(@Attribute('tabindex') tabindex: string) {
this.checked = false;
this.tabindex = isPresent(tabindex) ? NumberWrapper.parseInt(tabindex, 10) : 0;
this.tabindex = parseTabIndexAttribute(tabindex);
this.disabled_ = false;
}

Expand Down
78 changes: 41 additions & 37 deletions ng2-material/components/radio/radio_button.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ import {Event, KeyboardEvent} from 'angular2/src/facade/browser';
import {MdRadioDispatcher} from './radio_dispatcher';
import {KeyCodes} from '../../core/key_codes';
import {Output, Input} from 'angular2/core';
import {OnDestroy} from "angular2/core";
import {parseTabIndexAttribute} from "../../core/util/util";

// TODO(jdd): [disabled] style

// TODO(jelbourn): Behaviors to test
// Radios set default tab index iff not in parent group
// Radio name is pulled on parent group
// Radio group changes on arrow keys
// Radio group skips disabled radios on arrow keys

var _uniqueIdCounter: number = 0;

Expand All @@ -35,7 +33,6 @@ var _uniqueIdCounter: number = 0;
'role': 'radiogroup',
'[attr.aria-disabled]': 'disabled',
'[attr.aria-activedescendant]': 'activedescendant',
// TODO(jelbourn): Remove ^ when event retargeting is fixed.
'(keydown)': 'onKeydown($event)',
'[tabindex]': 'tabindex',
}
Expand All @@ -45,21 +42,21 @@ var _uniqueIdCounter: number = 0;
encapsulation: ViewEncapsulation.None
})
export class MdRadioGroup implements OnChanges {

@Output('valueChange')
change: EventEmitter<any> = new EventEmitter();

/** The selected value for the radio group. The value comes from the options. */
@Input('value') value_: any;
@Input('value')
value_: any;

get value(): any {
return this.value_;
}

set value(value: any) {
let button = this.getChildByValue(value);
this.value_ = value;
if (button) {
this.selectedRadioId = button.id;
this.activedescendant = button.id;
button.checked = true;
}
this._setChildValue(value);
}

/** The HTML name attribute applied to radio buttons in this group. */
Expand All @@ -78,7 +75,6 @@ export class MdRadioGroup implements OnChanges {
/** The ID of the selected radio button. */
selectedRadioId: string = '';

@Output('valueChange') change: EventEmitter<any> = new EventEmitter();

tabindex: number;

Expand All @@ -91,7 +87,7 @@ export class MdRadioGroup implements OnChanges {
this.disabled = isPresent(disabled);

// If the user has not set a tabindex, default to zero (in the normal document flow).
this.tabindex = isPresent(tabindex) ? NumberWrapper.parseInt(tabindex, 10) : 0;
this.tabindex = parseTabIndexAttribute(tabindex);
}

/** Gets the name of this group, as to be applied in the HTML 'name' attribute. */
Expand All @@ -116,13 +112,16 @@ export class MdRadioGroup implements OnChanges {
// child radio buttons and select the one that has a corresponding value (if any).
if (isPresent(this.value) && this.value !== '') {
this.radioDispatcher.notify(this.name_);
this.radios_.forEach(radio => {
if (radio.value === this.value) {
radio.checked = true;
this.selectedRadioId = radio.id;
this.activedescendant = radio.id;
}
});
this._setChildValue(this.value);
}
}

private _setChildValue(value: any) {
let button = this.getChildByValue(value);
if (button) {
this.selectedRadioId = button.id;
this.activedescendant = button.id;
button.checked = true;
}
}

Expand All @@ -138,6 +137,10 @@ export class MdRadioGroup implements OnChanges {
register(radio: MdRadioButton) {
this.radios_.push(radio);
}
/** Unregister a child radio button with this group. */
unregister(radio: MdRadioButton) {
this.radios_ = this.radios_.filter(r => r.id !== radio.id);
}

/** Handles up and down arrow key presses to change the selected child radio. */
onKeydown(event: KeyboardEvent) {
Expand Down Expand Up @@ -195,13 +198,8 @@ export class MdRadioGroup implements OnChanges {
return;
}

this.radioDispatcher.notify(this.name_);
this.updateValue(radio.value, radio.id);
radio.checked = true;
ObservableWrapper.callEmit(this.change, null);

this.value = radio.value;
this.selectedRadioId = radio.id;
this.activedescendant = radio.id;
}
}

Expand All @@ -222,24 +220,19 @@ export class MdRadioGroup implements OnChanges {
})
@View({
template: `
<!-- TODO(jelbourn): render the radio on either side of the content -->
<label role="radio" class="md-radio-root"
[class.md-radio-checked]="checked">
<!-- The actual radio part of the control. -->
<label role="radio" class="md-radio-root" [class.md-radio-checked]="checked">
<div class="md-radio-container">
<div class="md-radio-off"></div>
<div class="md-radio-on"></div>
</div>
<!-- The label for radio control. -->
<div class="md-radio-label">
<ng-content></ng-content>
<ng-content></ng-content>
</div>
</label>`,
directives: [],
encapsulation: ViewEncapsulation.None
})
export class MdRadioButton implements OnInit {
export class MdRadioButton implements OnInit, OnDestroy {
/** Whether this radio is checked. */
checked: boolean;

Expand All @@ -266,6 +259,7 @@ export class MdRadioButton implements OnInit {
constructor(@Optional() @SkipSelf() @Host() radioGroup: MdRadioGroup,
@Attribute('id') id: string,
@Attribute('value') value: string,
@Attribute('checked') checked: string,
@Attribute('tabindex') tabindex: string,
radioDispatcher: MdRadioDispatcher) {
// Assertions. Ideally these should be stripped out by the compiler.
Expand All @@ -274,7 +268,7 @@ export class MdRadioButton implements OnInit {
this.radioGroup = radioGroup;
this.radioDispatcher = radioDispatcher;
this.value = value ? value : null;
this.checked = false;
this.checked = isPresent(checked) ? true : false;

this.id = isPresent(id) ? id : `md-radio-${_uniqueIdCounter++}`;

Expand All @@ -289,11 +283,14 @@ export class MdRadioButton implements OnInit {
if (isPresent(radioGroup)) {
this.name = radioGroup.getName();
this.radioGroup.register(this);
if (this.checked) {
this.radioGroup.updateValue(this.value,this.id);
}
}

// If the user has not set a tabindex, default to zero (in the normal document flow).
if (!isPresent(radioGroup)) {
this.tabindex = isPresent(tabindex) ? NumberWrapper.parseInt(tabindex, 10) : 0;
this.tabindex = parseTabIndexAttribute(tabindex);
} else {
this.tabindex = -1;
}
Expand All @@ -306,6 +303,13 @@ export class MdRadioButton implements OnInit {
}
}

ngOnDestroy(): any {
if (isPresent(this.radioGroup)) {
this.radioGroup.unregister(this);
}
}


/** Whether this radio button is disabled, taking the parent group into account. */
isDisabled(): boolean {
// Here, this.disabled may be true/false as the result of a binding, may be the empty string
Expand Down
6 changes: 6 additions & 0 deletions ng2-material/core/util/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {isPresent} from "angular2/src/facade/lang";
import {NumberWrapper} from "angular2/src/facade/lang";
/**
* Returns a function, that, as long as it continues to be invoked, will not
* be triggered. The function will be called after it stops being called for
Expand Down Expand Up @@ -44,3 +46,7 @@ export function throttle(func, delay, scope) {
export function rAF(callback) {
window.requestAnimationFrame(callback);
}

export function parseTabIndexAttribute(attr: any): number {
return isPresent(attr) ? NumberWrapper.parseInt(attr, 10) : 0;
}
Loading

0 comments on commit 3aa8e0c

Please sign in to comment.