diff --git a/src/components/checkbox/checkbox.spec.ts b/src/components/checkbox/checkbox.spec.ts index 3abe395b2356..2a20daf7f976 100644 --- a/src/components/checkbox/checkbox.spec.ts +++ b/src/components/checkbox/checkbox.spec.ts @@ -289,9 +289,9 @@ export function main() { expect(el.nativeElement.className).toContain('md-checkbox-disabled'); }); - it('sets the tabindex to -1 on the host element', function() { + it('removes the tabindex attribute from the host element', function() { let el = fixture.debugElement.query(By.css('.md-checkbox')); - expect(el.nativeElement.getAttribute('tabindex')).toEqual('-1'); + expect(el.nativeElement.hasAttribute('tabindex')).toBe(false); }); it('sets "aria-disabled" to "true" on the host element', function() { @@ -307,7 +307,7 @@ export function main() { tabindexController.isDisabled = true; fixture.detectChanges(); let el = fixture.debugElement.query(By.css('.md-checkbox')); - expect(el.nativeElement.getAttribute('tabindex')).toEqual('-1'); + expect(el.nativeElement.hasAttribute('tabindex')).toBe(false); tabindexController.isDisabled = false; fixture.detectChanges(); @@ -334,9 +334,9 @@ export function main() { }).then(done).catch(done); }); - it('keeps the tabindex at -1', function() { + it('keeps the tabindex removed from the host', function() { let el = fixture.debugElement.query(By.css('.md-checkbox')); - expect(el.nativeElement.getAttribute('tabindex')).toEqual('-1'); + expect(el.nativeElement.hasAttribute('tabindex')).toBe(false); }); it('uses the newly changed tabindex when re-enabled', function() { @@ -394,6 +394,102 @@ export function main() { }); }); + describe('when the checkbox is focused', function() { + var browserSupportsDom3KeyProp: boolean = ({}).hasOwnProperty.call( + KeyboardEvent.prototype, 'key'); + var fixture: ComponentFixture; + var controller: CheckboxController; + var el: DebugElement; + var windowListenerSpy: jasmine.Spy; + var windowKeydownListeners: EventListener[]; + + function keydown(target: EventTarget, key: string, keyCode: number): Event { + var evt: Event; + if (BROWSER_SUPPORTS_EVENT_CONSTRUCTORS && browserSupportsDom3KeyProp) { + evt = new KeyboardEvent('keydown', {key}); + } else { + // NOTE(traviskaufman): Chrome/Webkit-based browsers do not support any mechanism for + // creating keyboard events with the correct keyCode. Therefore, the object.defineProperty + // hack is the only way to make this work correctly. + // see: https://bugs.webkit.org/show_bug.cgi?id=16735 + // see: https://bugs.chromium.org/p/chromium/issues/detail?id=227231 + // see: http://goo.gl/vrh534 (stackoverflow post on the topic). + evt = document.createEvent('Event'); + evt.initEvent('keydown', true, true); + Object.defineProperty(evt, 'keyCode', { + value: keyCode, + enumerable: true, + writable: false, + configurable: true + }); + } + spyOn(evt, 'preventDefault').and.callThrough(); + target.dispatchEvent(evt); + return evt; + }; + + function dispatchUIEventOnEl(evtName: string) { + var evt: Event; + if (BROWSER_SUPPORTS_EVENT_CONSTRUCTORS) { + evt = new Event(evtName); + } else { + evt = document.createEvent('Event'); + evt.initEvent(evtName, true, true); + } + el.nativeElement.dispatchEvent(evt); + fixture.detectChanges(); + } + + beforeEach(function(done: () => void) { + var windowAddEventListener = window.addEventListener.bind(window); + windowKeydownListeners = []; + windowListenerSpy = spyOn(window, 'addEventListener').and.callFake( + function(name: string, listener: EventListener) { + if (name === 'keydown') { + windowKeydownListeners.push(listener); + } + windowAddEventListener(name, listener); + }); + + builder.createAsync(CheckboxController).then(function(f) { + fixture = f; + controller = fixture.componentInstance; + + fixture.detectChanges(); + el = fixture.debugElement.query(By.css('.md-checkbox')); + }).then(done).catch(done); + }); + + afterEach(function() { + windowKeydownListeners.forEach((l) => window.removeEventListener('keydown', l)); + }); + + it('blocks spacebar keydown events from performing their default behavior', function() { + dispatchUIEventOnEl('focus'); + + var evt = keydown(window, ' ', 32); + expect(evt.preventDefault).toHaveBeenCalled(); + }); + + it('does not block other keyboard events from performing their default behavior', function() { + dispatchUIEventOnEl('focus'); + + var evt = keydown(window, 'Tab', 9); + expect(evt.preventDefault).not.toHaveBeenCalled(); + }); + + it('stops blocking spacebar keydown events when un-focused', function() { + dispatchUIEventOnEl('focus'); + // sanity check. + var evt = keydown(window, ' ', 32); + expect(evt.preventDefault).toHaveBeenCalled(); + + dispatchUIEventOnEl('blur'); + evt = keydown(window, ' ', 32); + expect(evt.preventDefault).not.toHaveBeenCalled(); + }); + }); + describe('when a spacebar press occurs on the checkbox', function() { var fixture: ComponentFixture; var controller: CheckboxController; @@ -602,10 +698,11 @@ function keyup(el: DebugElement, key: string, fixture: ComponentFixture) { if (BROWSER_SUPPORTS_EVENT_CONSTRUCTORS) { kbdEvent = new KeyboardEvent('keyup'); } else { - kbdEvent = document.createEvent('Events'); + kbdEvent = document.createEvent('Event'); kbdEvent.initEvent('keyup', true, true); } // Hack DOM Level 3 Events "key" prop into keyboard event. + // See note above for explanation of this defineProperty hack. Object.defineProperty(kbdEvent, 'key', { value: ' ', enumerable: false, diff --git a/src/components/checkbox/checkbox.ts b/src/components/checkbox/checkbox.ts index 0c8c06e6d100..0ff21e52acf1 100644 --- a/src/components/checkbox/checkbox.ts +++ b/src/components/checkbox/checkbox.ts @@ -68,14 +68,18 @@ enum TransitionCheckState { '[class.md-checkbox-checked]': 'checked', '[class.md-checkbox-disabled]': 'disabled', '[class.md-checkbox-align-end]': 'align == "end"', - '[tabindex]': 'disabled ? -1 : tabindex', + '[attr.tabindex]': 'disabled ? null : tabindex', '[attr.aria-label]': 'ariaLabel', '[attr.aria-labelledby]': 'labelId', '[attr.aria-checked]': 'getAriaChecked()', '[attr.aria-disabled]': 'disabled', '(click)': 'onInteractionEvent($event)', '(keyup.space)': 'onInteractionEvent($event)', - '(blur)': 'onTouched()' + '(focus)': 'onFocus($event)', + '(blur)': 'onBlur()', + // TODO(traviskaufman): Use (window:keydown.space) if/when it's supported. + // See: https://github.com/angular/angular/issues/7308 + '(window:keydown)': 'onWindowKeyDown($event)' }, providers: [MD_CHECKBOX_CONTROL_VALUE_ACCESSOR], encapsulation: ViewEncapsulation.None, @@ -102,7 +106,7 @@ export class MdCheckbox implements ControlValueAccessor { /** * The tabindex attribute for the checkbox. Note that when the checkbox is disabled, the attribute - * on the host element will be set to -1, regardless of the actual tabindex value. + * on the host element will be removed. It will be placed back when the checkbox is re-enabled. */ @Input() tabindex: number = 0; @@ -120,6 +124,8 @@ export class MdCheckbox implements ControlValueAccessor { private _indeterminate: boolean = false; + private _isFocused: boolean = false; + private _changeSubscription: {unsubscribe: () => any} = null; constructor(private _renderer: Renderer, private _elementRef: ElementRef) {} @@ -192,6 +198,33 @@ export class MdCheckbox implements ControlValueAccessor { this.toggle(); } + /** + * Event handler used for (focus) events. + */ + onFocus(evt: Event) { + this._isFocused = true; + } + + /** + * Event handler used for (blur) events. + */ + onBlur() { + this._isFocused = false; + this.onTouched(); + } + + /** + * Event handler used for (keydown.space) events on the window. Used to prevent spacebar events + * from bubbling when the component is focused, which prevents side effects like page scrolling + * from happening. + */ + onWindowKeyDown(evt: KeyboardEvent) { + let wasSpacePressed = evt.key && evt.key === ' ' || evt.keyCode === 32; + if (this._isFocused && wasSpacePressed) { + evt.preventDefault(); + } + } + /** Implemented as part of ControlValueAccessor. */ writeValue(value: any) { this.checked = !!value;