diff --git a/src/components/radio/radio.spec.ts b/src/components/radio/radio.spec.ts
index ca408650f200..1ec336ee1966 100644
--- a/src/components/radio/radio.spec.ts
+++ b/src/components/radio/radio.spec.ts
@@ -61,9 +61,11 @@ export function main() {
.createAsync(TestApp)
.then(fixture => {
let button = fixture.debugElement.query(By.css('md-radio-button'));
+ let input = button.query(By.css('input'));
fixture.detectChanges();
expect(button.componentInstance.disabled).toBe(true);
+ expect(input.nativeElement.hasAttribute('tabindex')).toBe(false);
}).then(done);
});
@@ -149,6 +151,33 @@ export function main() {
expect(button.nativeElement.classList.contains('md-radio-focused')).toBe(false);
}).then(done);
});
+ it('should not scroll when pressing space on the checkbox', (done: () => void) => {
+ builder
+ .overrideTemplate(TestApp, '')
+ .createAsync(TestApp)
+ .then(fixture => {
+ let button = fixture.debugElement.query(By.css('md-radio-button'));
+
+ let keyboardEvent = dispatchKeyboardEvent('keydown', button.nativeElement, ' ');
+ fixture.detectChanges();
+
+ expect(keyboardEvent.preventDefault).toHaveBeenCalled();
+ }).then(done);
+ });
+ it('should make the host element a tab stop', (done: () => void) => {
+ builder
+ .overrideTemplate(TestApp, `
+
+
+
+ `)
+ .createAsync(TestApp)
+ .then(fixture => {
+ let button = fixture.debugElement.query(By.css('md-radio-button'));
+ fixture.detectChanges();
+ expect(button.nativeElement.tabIndex).toBe(0);
+ }).then(done);
+ });
});
describe('MdRadioDispatcher', () => {
@@ -323,7 +352,28 @@ export function main() {
});
}).then(done);
});
+ it('should deselect all buttons when model is null or undefined', (done: () => void) => {
+ builder
+ .overrideTemplate(TestAppWithInitialValue, `
+
+
+
+ `)
+ .createAsync(TestAppWithInitialValue)
+ .then(fixture => {
+ fakeAsync(function() {
+ let buttons = fixture.debugElement.queryAll(By.css('md-radio-button'));
+ fixture.detectChanges();
+ fixture.componentInstance.choice = 0;
+ expect(isSinglySelected(buttons[0], buttons)).toBe(true);
+
+ fixture.detectChanges();
+ fixture.componentInstance.choice = null;
+ expect(allDeselected(buttons)).toBe(true);
+ });
+ }).then(done);
+ });
});
}
@@ -336,6 +386,13 @@ function isSinglySelected(button: DebugElement, buttons: DebugElement[]): boolea
return component.checked && otherSelectedButtons.length == 0;
}
+/** Checks whether no button is selected from a group of buttons. */
+function allDeselected(buttons: DebugElement[]): boolean {
+ let selectedButtons =
+ buttons.filter((e: DebugElement) => e.componentInstance.checked);
+ return selectedButtons.length == 0;
+}
+
/** Browser-agnostic function for creating an event. */
function createEvent(name: string): Event {
let ev: Event;
@@ -368,3 +425,50 @@ class TestApp {
class TestAppWithInitialValue {
choice: number = 1;
}
+
+
+// TODO(trik): remove eveything below when Angular supports faking events.
+// copy & paste from checkbox.spec.ts
+
+
+var BROWSER_SUPPORTS_EVENT_CONSTRUCTORS: boolean = (function() {
+ // See: https://github.com/rauschma/event_constructors_check/blob/gh-pages/index.html#L39
+ try {
+ return new Event('submit', { bubbles: false }).bubbles === false &&
+ new Event('submit', { bubbles: true }).bubbles === true;
+ } catch (e) {
+ return false;
+ }
+})();
+
+/**
+ * Dispatches a keyboard event from an element.
+ * @param eventName The name of the event to dispatch, such as "keydown".
+ * @param element The element from which the event will be dispatched.
+ * @param key The key tied to the KeyboardEvent.
+ * @returns The artifically created keyboard event.
+ */
+function dispatchKeyboardEvent(eventName: string, element: HTMLElement, key: string): Event {
+ let keyboardEvent: Event;
+ if (BROWSER_SUPPORTS_EVENT_CONSTRUCTORS) {
+ keyboardEvent = new KeyboardEvent(eventName);
+ } else {
+ keyboardEvent = document.createEvent('Event');
+ keyboardEvent.initEvent(eventName, true, true);
+ }
+
+ // Hack DOM Level 3 Events "key" prop into keyboard event.
+ Object.defineProperty(keyboardEvent, 'key', {
+ value: key,
+ enumerable: false,
+ writable: false,
+ configurable: true,
+ });
+
+ // Using spyOn seems to be the *only* way to determine if preventDefault is called, since it
+ // seems that `defaultPrevented` does not get set with the technique.
+ spyOn(keyboardEvent, 'preventDefault').and.callThrough();
+
+ element.dispatchEvent(keyboardEvent);
+ return keyboardEvent;
+}
diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts
index 55f4a6207aaf..09740e6da98d 100644
--- a/src/components/radio/radio.ts
+++ b/src/components/radio/radio.ts
@@ -150,7 +150,10 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
});
if (matched.length == 0) {
- // Didn't find a button that matches this value, return early without setting.
+ // Didn't find a button that matches this value, deselecting all buttons.
+ if (this._value == null) {
+ this.selected = null;
+ }
return;
}
@@ -206,7 +209,11 @@ export class MdRadioGroup implements AfterContentInit, ControlValueAccessor {
styleUrls: ['./components/radio/radio.css'],
encapsulation: ViewEncapsulation.None,
host: {
- '(click)': 'onClick($event)'
+ '[id]': 'id',
+ '[attr.tabindex]': 'disabled ? null : tabindex',
+ '(keydown.space)': 'onSpaceDown($event)',
+ '(keyup.space)': 'onInteractionEvent($event)',
+ '(click)': 'onInteractionEvent($event)'
}
})
export class MdRadioButton implements OnInit {
@@ -225,6 +232,13 @@ export class MdRadioButton implements OnInit {
@Input()
name: string;
+ /**
+ * The tabindex attribute for the radio button. Note that when the checkbox is disabled,
+ * the attribute on the host element will be removed. It will be placed back when the
+ * radio button is re-enabled.
+ */
+ @Input() tabindex: number = 0;
+
/** Whether this radio is disabled. */
private _disabled: boolean;
@@ -335,7 +349,15 @@ export class MdRadioButton implements OnInit {
this._disabled = (value != null && value !== false) ? true : null;
}
- onClick(event: Event) {
+ /**
+ * Event handler used for (keydown.space) events. Used to prevent spacebar events from bubbling
+ * when the component is focused, which prevents side effects like page scrolling from happening.
+ */
+ onSpaceDown(evt: Event) {
+ evt.preventDefault();
+ }
+
+ onInteractionEvent(event: Event) {
if (this.disabled) {
event.preventDefault();
event.stopPropagation();