Skip to content

Commit 42d0d54

Browse files
authored
Add focus to checkbox, radio, button-toggle and slide-toggle (#1775)
1 parent 93807ed commit 42d0d54

File tree

10 files changed

+90
-2
lines changed

10 files changed

+90
-2
lines changed

src/demo-app/slide-toggle/slide-toggle-demo.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@
2626

2727
</form>
2828

29-
</div>
29+
</div>

src/lib/button-toggle/button-toggle.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,16 @@ describe('MdButtonToggle', () => {
518518
expect(changeSpy).toHaveBeenCalledTimes(2);
519519
}));
520520

521+
it('should focus on underlying input element when focus() is called', () => {
522+
let nativeRadioInput = buttonToggleDebugElement.query(By.css('input')).nativeElement;
523+
expect(document.activeElement).not.toBe(nativeRadioInput);
524+
525+
buttonToggleInstance.focus();
526+
fixture.detectChanges();
527+
528+
expect(document.activeElement).toBe(nativeRadioInput);
529+
});
530+
521531
});
522532
});
523533

src/lib/button-toggle/button-toggle.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import {
44
Component,
55
ContentChildren,
66
Directive,
7+
ElementRef,
78
EventEmitter,
89
HostBinding,
910
Input,
1011
OnInit,
1112
Optional,
1213
Output,
1314
QueryList,
15+
ViewChild,
1416
ViewEncapsulation,
1517
forwardRef,
1618
AfterViewInit
@@ -295,6 +297,8 @@ export class MdButtonToggle implements OnInit {
295297
return this._change.asObservable();
296298
}
297299

300+
@ViewChild('input') _inputElement: ElementRef;
301+
298302
constructor(@Optional() toggleGroup: MdButtonToggleGroup,
299303
@Optional() toggleGroupMultiple: MdButtonToggleGroupMultiple,
300304
public buttonToggleDispatcher: MdUniqueSelectionDispatcher) {
@@ -424,6 +428,10 @@ export class MdButtonToggle implements OnInit {
424428
// Preventing bubbling for the second event will solve that issue.
425429
event.stopPropagation();
426430
}
431+
432+
focus() {
433+
this._inputElement.nativeElement.focus();
434+
}
427435
}
428436

429437

src/lib/checkbox/checkbox.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<label class="md-checkbox-layout">
22
<div class="md-checkbox-inner-container">
3-
<input class="md-checkbox-input md-visually-hidden" type="checkbox"
3+
<input #input
4+
class="md-checkbox-input md-visually-hidden" type="checkbox"
45
[id]="inputId"
56
[required]="required"
67
[checked]="checked"

src/lib/checkbox/checkbox.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,15 @@ describe('MdCheckbox', () => {
281281
expect(inputElement.required).toBe(false);
282282
});
283283

284+
it('should focus on underlying input element when focus() is called', () => {
285+
expect(document.activeElement).not.toBe(inputElement);
286+
287+
checkboxInstance.focus();
288+
fixture.detectChanges();
289+
290+
expect(document.activeElement).toBe(inputElement);
291+
});
292+
284293
describe('color behaviour', () => {
285294
it('should apply class based on color attribute', () => {
286295
testComponent.checkboxColor = 'primary';

src/lib/checkbox/checkbox.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
forwardRef,
1111
NgModule,
1212
ModuleWithProviders,
13+
ViewChild,
1314
} from '@angular/core';
1415
import {CommonModule} from '@angular/common';
1516
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
@@ -134,6 +135,9 @@ export class MdCheckbox implements ControlValueAccessor {
134135
/** Event emitted when the checkbox's `checked` value changes. */
135136
@Output() change: EventEmitter<MdCheckboxChange> = new EventEmitter<MdCheckboxChange>();
136137

138+
/** The native `<input type=checkbox> element */
139+
@ViewChild('input') _inputElement: ElementRef;
140+
137141
/** Called when the checkbox is blurred. Needed to properly implement ControlValueAccessor. */
138142
onTouched: () => any = () => {};
139143

@@ -321,6 +325,11 @@ export class MdCheckbox implements ControlValueAccessor {
321325
}
322326
}
323327

328+
focus() {
329+
this._inputElement.nativeElement.focus();
330+
this._onInputFocus();
331+
}
332+
324333
_onInputClick(event: Event) {
325334
// We have to stop propagation for click events on the visual hidden input element.
326335
// By default, when a user clicks on a label element, a generated click event will be

src/lib/radio/radio.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,20 @@ describe('MdRadio', () => {
189189
expect(radioNativeElements[0].classList).not.toContain('md-radio-focused');
190190
});
191191

192+
it('should focus individual radio buttons', () => {
193+
let nativeRadioInput = <HTMLElement> radioNativeElements[0].querySelector('input');
194+
195+
radioInstances[0].focus();
196+
fixture.detectChanges();
197+
198+
expect(radioNativeElements[0].classList).toContain('md-radio-focused');
199+
200+
dispatchEvent('blur', nativeRadioInput);
201+
fixture.detectChanges();
202+
203+
expect(radioNativeElements[0].classList).not.toContain('md-radio-focused');
204+
});
205+
192206
it('should update the group and radios when updating the group value', () => {
193207
expect(groupInstance.value).toBeFalsy();
194208

@@ -550,6 +564,16 @@ describe('MdRadio', () => {
550564

551565
expect(fruitRadioNativeInputs[0].getAttribute('aria-labelledby')).toBe('uvw');
552566
});
567+
568+
it('should focus on underlying input element when focus() is called', () => {
569+
for (let i = 0; i < fruitRadioInstances.length; i++) {
570+
expect(document.activeElement).not.toBe(fruitRadioNativeInputs[i]);
571+
fruitRadioInstances[i].focus();
572+
fixture.detectChanges();
573+
574+
expect(document.activeElement).toBe(fruitRadioNativeInputs[i]);
575+
}
576+
});
553577
});
554578
});
555579

src/lib/radio/radio.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
forwardRef,
1616
NgModule,
1717
ModuleWithProviders,
18+
ViewChild,
1819
} from '@angular/core';
1920
import {CommonModule} from '@angular/common';
2021
import {
@@ -288,6 +289,9 @@ export class MdRadioButton implements OnInit {
288289
@Output()
289290
change: EventEmitter<MdRadioChange> = new EventEmitter<MdRadioChange>();
290291

292+
/** The native `<input type=radio> element */
293+
@ViewChild('input') _inputElement: ElementRef;
294+
291295
constructor(@Optional() radioGroup: MdRadioGroup,
292296
private _elementRef: ElementRef,
293297
public radioDispatcher: MdUniqueSelectionDispatcher) {
@@ -407,6 +411,11 @@ export class MdRadioButton implements OnInit {
407411
this._isFocused = true;
408412
}
409413

414+
focus() {
415+
this._inputElement.nativeElement.focus();
416+
this._onInputFocus();
417+
}
418+
410419
/** TODO: internal */
411420
_onInputBlur() {
412421
this._isFocused = false;

src/lib/slide-toggle/slide-toggle.spec.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,16 @@ describe('MdSlideToggle', () => {
336336
expect(inputElement.required).toBe(false);
337337
});
338338

339+
it('should focus on underlying input element when focus() is called', () => {
340+
expect(slideToggleElement.classList).not.toContain('md-slide-toggle-focused');
341+
expect(document.activeElement).not.toBe(inputElement);
342+
343+
slideToggle.focus();
344+
fixture.detectChanges();
345+
346+
expect(document.activeElement).toBe(inputElement);
347+
expect(slideToggleElement.classList).toContain('md-slide-toggle-focused');
348+
});
339349
});
340350

341351
describe('custom template', () => {

src/lib/slide-toggle/slide-toggle.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
AfterContentInit,
1111
NgModule,
1212
ModuleWithProviders,
13+
ViewChild,
1314
ViewEncapsulation,
1415
} from '@angular/core';
1516
import {HAMMER_GESTURE_CONFIG} from '@angular/platform-browser';
@@ -86,6 +87,8 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
8687
// Returns the unique id for the visual hidden input.
8788
getInputId = () => `${this.id || this._uniqueId}-input`;
8889

90+
@ViewChild('input') _inputElement: ElementRef;
91+
8992
constructor(private _elementRef: ElementRef, private _renderer: Renderer) {}
9093

9194
/** TODO: internal */
@@ -181,6 +184,11 @@ export class MdSlideToggle implements AfterContentInit, ControlValueAccessor {
181184
this.disabled = isDisabled;
182185
}
183186

187+
focus() {
188+
this._inputElement.nativeElement.focus();
189+
this._onInputFocus();
190+
}
191+
184192
@Input()
185193
get checked() {
186194
return !!this._checked;

0 commit comments

Comments
 (0)