mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(checkbox): drive component with native checkbox (#415)
Also fixes lack of visual focus state. closes #250
This commit is contained in:
committed by
Jeremy Elbourn
parent
7323e89dde
commit
10ac2be980
@@ -1,5 +1,18 @@
|
||||
<div class="md-checkbox-layout">
|
||||
<label class="md-checkbox-layout">
|
||||
<div class="md-checkbox-inner-container">
|
||||
<input class="md-checkbox-input" type="checkbox"
|
||||
[id]="inputId"
|
||||
[checked]="checked"
|
||||
[disabled]="disabled"
|
||||
[name]="name"
|
||||
[tabIndex]="tabindex"
|
||||
[indeterminate]="indeterminate"
|
||||
[attr.aria-label]="ariaLabel"
|
||||
[attr.aria-labelledby]="ariaLabelledby"
|
||||
(focus)="onInputFocus()"
|
||||
(blur)="onInputBlur()"
|
||||
(change)="onInteractionEvent($event)">
|
||||
<div class="md-ink-ripple"></div>
|
||||
<div class="md-checkbox-frame"></div>
|
||||
<div class="md-checkbox-background">
|
||||
<svg version="1.1"
|
||||
@@ -16,7 +29,7 @@
|
||||
<div class="md-checkbox-mixedmark"></div>
|
||||
</div>
|
||||
</div>
|
||||
<label [id]="labelId">
|
||||
<span class="md-checkbox-label">
|
||||
<ng-content></ng-content>
|
||||
</label>
|
||||
</div>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import "default-theme";
|
||||
@import "theme-functions";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
|
||||
/** The width/height of the checkbox element. */
|
||||
$md-checkbox-size: $md-toggle-size !default;
|
||||
@@ -217,11 +218,8 @@ $_md-checkbox-indeterminate-checked-easing-function: cubic-bezier(0.14, 0, 0, 1)
|
||||
}
|
||||
|
||||
md-checkbox {
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
// TODO(traviskaufman): Add ink ripple on focus state, once ripple is implemented.
|
||||
outline: none;
|
||||
&, label {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +249,7 @@ md-checkbox {
|
||||
}
|
||||
|
||||
// TODO(kara): Remove this style when fixing vertical baseline
|
||||
.md-checkbox-layout label {
|
||||
.md-checkbox-layout .md-checkbox-label {
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
@@ -461,3 +459,11 @@ md-checkbox {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Underlying native input element.
|
||||
// Visually hidden but still able to respond to focus.
|
||||
.md-checkbox-input {
|
||||
@include md-visually-hidden;
|
||||
}
|
||||
|
||||
@include md-temporary-ink-ripple(checkbox);
|
||||
|
||||
@@ -13,7 +13,7 @@ import {By} from '@angular/platform-browser';
|
||||
import {MdCheckbox} from './checkbox';
|
||||
import {PromiseCompleter} from '../../core/async/promise-completer';
|
||||
|
||||
|
||||
// TODO: Implement E2E tests for spacebar/click behavior for checking/unchecking
|
||||
|
||||
describe('MdCheckbox', () => {
|
||||
let builder: TestComponentBuilder;
|
||||
@@ -28,6 +28,8 @@ describe('MdCheckbox', () => {
|
||||
let checkboxNativeElement: HTMLElement;
|
||||
let checkboxInstance: MdCheckbox;
|
||||
let testComponent: SingleCheckbox;
|
||||
let inputElement: HTMLInputElement;
|
||||
let labelElement: HTMLLabelElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
builder.createAsync(SingleCheckbox).then(f => {
|
||||
@@ -38,71 +40,79 @@ describe('MdCheckbox', () => {
|
||||
checkboxNativeElement = checkboxDebugElement.nativeElement;
|
||||
checkboxInstance = checkboxDebugElement.componentInstance;
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
inputElement = <HTMLInputElement>checkboxNativeElement.querySelector('input');
|
||||
labelElement = <HTMLLabelElement>checkboxNativeElement.querySelector('label');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should add and remove the checked state', () => {
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked');
|
||||
expect(checkboxNativeElement.getAttribute('aria-checked')).toBe('false');
|
||||
expect(inputElement.checked).toBe(false);
|
||||
|
||||
testComponent.isChecked = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(true);
|
||||
expect(checkboxNativeElement.classList).toContain('md-checkbox-checked');
|
||||
expect(checkboxNativeElement.getAttribute('aria-checked')).toBe('true');
|
||||
expect(inputElement.checked).toBe(true);
|
||||
|
||||
testComponent.isChecked = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked');
|
||||
expect(checkboxNativeElement.getAttribute('aria-checked')).toBe('false');
|
||||
expect(inputElement.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('should add and remove indeterminate state', () => {
|
||||
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-checked');
|
||||
expect(checkboxNativeElement.getAttribute('aria-checked')).toBe('false');
|
||||
expect(inputElement.checked).toBe(false);
|
||||
expect(inputElement.indeterminate).toBe(false);
|
||||
|
||||
testComponent.isIndeterminate = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxNativeElement.classList).toContain('md-checkbox-indeterminate');
|
||||
expect(checkboxNativeElement.getAttribute('aria-checked')).toBe('mixed');
|
||||
expect(inputElement.checked).toBe(false);
|
||||
expect(inputElement.indeterminate).toBe(true);
|
||||
|
||||
testComponent.isIndeterminate = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-indeterminate');
|
||||
expect(checkboxNativeElement.getAttribute('aria-checked')).toBe('false');
|
||||
expect(inputElement.checked).toBe(false);
|
||||
expect(inputElement.indeterminate).toBe(false);
|
||||
});
|
||||
|
||||
it('should toggle checked state on click', () => {
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
|
||||
checkboxNativeElement.click();
|
||||
labelElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(true);
|
||||
|
||||
checkboxNativeElement.click();
|
||||
labelElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('should change from indeterminate to checked on click', () => {
|
||||
testComponent.isChecked = false;
|
||||
testComponent.isIndeterminate = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
checkboxNativeElement.click();
|
||||
fixture.detectChanges();
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
expect(checkboxInstance.indeterminate).toBe(true);
|
||||
|
||||
checkboxInstance.onInteractionEvent(<Event>{stopPropagation: () => {}});
|
||||
|
||||
expect(checkboxInstance.checked).toBe(true);
|
||||
expect(checkboxInstance.indeterminate).toBe(false);
|
||||
|
||||
checkboxNativeElement.click();
|
||||
checkboxInstance.onInteractionEvent(<Event>{stopPropagation: () => {}});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
@@ -112,21 +122,23 @@ describe('MdCheckbox', () => {
|
||||
it('should add and remove disabled state', () => {
|
||||
expect(checkboxInstance.disabled).toBe(false);
|
||||
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-disabled');
|
||||
expect(checkboxNativeElement.tabIndex).toBe(0);
|
||||
expect(inputElement.tabIndex).toBe(0);
|
||||
expect(inputElement.disabled).toBe(false);
|
||||
|
||||
testComponent.isDisabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.disabled).toBe(true);
|
||||
expect(checkboxNativeElement.classList).toContain('md-checkbox-disabled');
|
||||
expect(checkboxNativeElement.hasAttribute('tabindex')).toBe(false);
|
||||
expect(inputElement.disabled).toBe(true);
|
||||
|
||||
testComponent.isDisabled = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.disabled).toBe(false);
|
||||
expect(checkboxNativeElement.classList).not.toContain('md-checkbox-disabled');
|
||||
expect(checkboxNativeElement.tabIndex).toBe(0);
|
||||
expect(inputElement.tabIndex).toBe(0);
|
||||
expect(inputElement.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it('should not toggle `checked` state upon interation while disabled', () => {
|
||||
@@ -152,25 +164,13 @@ describe('MdCheckbox', () => {
|
||||
expect(checkboxNativeElement.id).toBe('simple-check');
|
||||
});
|
||||
|
||||
it('should create a label element with its own unique id for aria-labelledby', () => {
|
||||
let labelElement = checkboxNativeElement.querySelector('label');
|
||||
expect(labelElement.id).toBeTruthy();
|
||||
expect(labelElement.id).not.toBe(checkboxNativeElement.id);
|
||||
expect(checkboxNativeElement.getAttribute('aria-labelledby')).toBe(labelElement.id);
|
||||
});
|
||||
|
||||
it('should project the checkbox content into the label element', () => {
|
||||
let labelElement = checkboxNativeElement.querySelector('label');
|
||||
|
||||
expect(labelElement.textContent.trim()).toBe('Simple checkbox');
|
||||
});
|
||||
|
||||
it('should mark the host element with role="checkbox"', () => {
|
||||
expect(checkboxNativeElement.getAttribute('role')).toBe('checkbox');
|
||||
let label = <HTMLLabelElement>checkboxNativeElement.querySelector('.md-checkbox-label');
|
||||
expect(label.textContent.trim()).toBe('Simple checkbox');
|
||||
});
|
||||
|
||||
it('should make the host element a tab stop', () => {
|
||||
expect(checkboxNativeElement.tabIndex).toBe(0);
|
||||
expect(inputElement.tabIndex).toBe(0);
|
||||
});
|
||||
|
||||
it('should add a css class to end-align the checkbox', () => {
|
||||
@@ -196,46 +196,6 @@ describe('MdCheckbox', () => {
|
||||
return promiseCompleter.promise;
|
||||
});
|
||||
|
||||
it('should stop propagation of interaction events when disabed', () => {
|
||||
testComponent.isDisabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
checkboxNativeElement.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(testComponent.parentElementClicked).toBe(false);
|
||||
});
|
||||
|
||||
it('should not scroll when pressing space on the checkbox', () => {
|
||||
let keyboardEvent = dispatchKeyboardEvent('keydown', checkboxNativeElement, ' ');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(keyboardEvent.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should toggle the checked state when pressing space', () => {
|
||||
dispatchKeyboardEvent('keyup', checkboxNativeElement, ' ');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(true);
|
||||
|
||||
dispatchKeyboardEvent('keyup', checkboxNativeElement, ' ');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
});
|
||||
|
||||
it('should not toggle the checked state when pressing space if disabled', () => {
|
||||
testComponent.isDisabled = true;
|
||||
fixture.detectChanges();
|
||||
|
||||
dispatchKeyboardEvent('keyup', checkboxNativeElement, ' ');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxInstance.checked).toBe(false);
|
||||
expect(testComponent.parentElementKeyedUp).toBe(false);
|
||||
});
|
||||
|
||||
describe('state transition css classes', () => {
|
||||
it('should transition unchecked -> checked -> unchecked', () => {
|
||||
testComponent.isChecked = true;
|
||||
@@ -294,14 +254,47 @@ describe('MdCheckbox', () => {
|
||||
describe('with provided aria-label ', () => {
|
||||
let checkboxDebugElement: DebugElement;
|
||||
let checkboxNativeElement: HTMLElement;
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
it('should use the provided aria-label', async(() => {
|
||||
builder.createAsync(CheckboxWithAriaLabel).then(f => {
|
||||
fixture = f;
|
||||
checkboxDebugElement = fixture.debugElement.query(By.directive(MdCheckbox));
|
||||
checkboxNativeElement = checkboxDebugElement.nativeElement;
|
||||
inputElement = <HTMLInputElement>checkboxNativeElement.querySelector('input');
|
||||
|
||||
expect(checkboxNativeElement.getAttribute('aria-label')).toBe('Super effective');
|
||||
fixture.detectChanges();
|
||||
expect(inputElement.getAttribute('aria-label')).toBe('Super effective');
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with provided aria-labelledby ', () => {
|
||||
let checkboxDebugElement: DebugElement;
|
||||
let checkboxNativeElement: HTMLElement;
|
||||
let inputElement: HTMLInputElement;
|
||||
|
||||
it('should use the provided aria-labelledby', async(() => {
|
||||
builder.createAsync(CheckboxWithAriaLabelledby).then(f => {
|
||||
fixture = f;
|
||||
checkboxDebugElement = fixture.debugElement.query(By.directive(MdCheckbox));
|
||||
checkboxNativeElement = checkboxDebugElement.nativeElement;
|
||||
inputElement = <HTMLInputElement>checkboxNativeElement.querySelector('input');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(inputElement.getAttribute('aria-labelledby')).toBe('some-id');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not assign aria-labelledby if none is provided', async(() => {
|
||||
builder.createAsync(SingleCheckbox).then(f => {
|
||||
fixture = f;
|
||||
checkboxDebugElement = fixture.debugElement.query(By.directive(MdCheckbox));
|
||||
checkboxNativeElement = checkboxDebugElement.nativeElement;
|
||||
inputElement = <HTMLInputElement>checkboxNativeElement.querySelector('input');
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(inputElement.getAttribute('aria-labelledby')).toBe(null);
|
||||
});
|
||||
}));
|
||||
});
|
||||
@@ -310,6 +303,8 @@ describe('MdCheckbox', () => {
|
||||
let checkboxDebugElement: DebugElement;
|
||||
let checkboxNativeElement: HTMLElement;
|
||||
let testComponent: CheckboxWithTabIndex;
|
||||
let inputElement: HTMLInputElement;
|
||||
let labelElement: HTMLLabelElement;
|
||||
|
||||
beforeEach(async(() => {
|
||||
builder.createAsync(CheckboxWithTabIndex).then(f => {
|
||||
@@ -319,11 +314,13 @@ describe('MdCheckbox', () => {
|
||||
testComponent = fixture.debugElement.componentInstance;
|
||||
checkboxDebugElement = fixture.debugElement.query(By.directive(MdCheckbox));
|
||||
checkboxNativeElement = checkboxDebugElement.nativeElement;
|
||||
inputElement = <HTMLInputElement>checkboxNativeElement.querySelector('input');
|
||||
labelElement = <HTMLLabelElement>checkboxNativeElement.querySelector('label');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should preserve any given tabIndex', async(() => {
|
||||
expect(checkboxNativeElement.tabIndex).toBe(7);
|
||||
expect(inputElement.tabIndex).toBe(7);
|
||||
}));
|
||||
|
||||
it('should preserve given tabIndex when the checkbox is disabled then enabled', () => {
|
||||
@@ -336,7 +333,7 @@ describe('MdCheckbox', () => {
|
||||
testComponent.isDisabled = false;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(checkboxNativeElement.tabIndex).toBe(13);
|
||||
expect(inputElement.tabIndex).toBe(13);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -351,7 +348,7 @@ describe('MdCheckbox', () => {
|
||||
it('should assign a unique id to each checkbox', () => {
|
||||
let [firstId, secondId] =
|
||||
fixture.debugElement.queryAll(By.directive(MdCheckbox))
|
||||
.map(debugElement => debugElement.nativeElement.id);
|
||||
.map(debugElement => debugElement.nativeElement.querySelector('input').id);
|
||||
|
||||
expect(firstId).toBeTruthy();
|
||||
expect(secondId).toBeTruthy();
|
||||
@@ -382,8 +379,22 @@ describe('MdCheckbox', () => {
|
||||
}));
|
||||
});
|
||||
|
||||
});
|
||||
describe('with name attribute', () => {
|
||||
beforeEach(async(() => {
|
||||
builder.createAsync(CheckboxWithNameAttribute).then(f => {
|
||||
f.detectChanges();
|
||||
fixture = f;
|
||||
});
|
||||
}));
|
||||
|
||||
it('should forward name value to input element', fakeAsync(() => {
|
||||
let checkboxElement = fixture.debugElement.query(By.directive(MdCheckbox));
|
||||
let inputElement = <HTMLInputElement> checkboxElement.nativeElement.querySelector('input');
|
||||
|
||||
expect(inputElement.getAttribute('name')).toBe('test-name');
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
/** Simple component for testing a single checkbox. */
|
||||
@Component({
|
||||
@@ -454,49 +465,16 @@ class CheckboxWithTabIndex {
|
||||
})
|
||||
class CheckboxWithAriaLabel { }
|
||||
|
||||
// TODO(jelbourn): remove eveything below when Angular supports faking events.
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
/** Simple test component with an aria-label set. */
|
||||
@Component({
|
||||
directives: [MdCheckbox],
|
||||
template: `<md-checkbox aria-labelledby="some-id"></md-checkbox>`
|
||||
})
|
||||
class CheckboxWithAriaLabelledby {}
|
||||
|
||||
/** Simple test component with name attribute */
|
||||
@Component({
|
||||
directives: [MdCheckbox],
|
||||
template: `<md-checkbox name="test-name"></md-checkbox>`
|
||||
})
|
||||
class CheckboxWithNameAttribute {}
|
||||
|
||||
@@ -15,8 +15,6 @@ import {
|
||||
ControlValueAccessor,
|
||||
} from '@angular/common';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Monotonically increasing integer used to auto-generate unique ids for checkbox components.
|
||||
*/
|
||||
@@ -59,21 +57,11 @@ enum TransitionCheckState {
|
||||
templateUrl: './components/checkbox/checkbox.html',
|
||||
styleUrls: ['./components/checkbox/checkbox.css'],
|
||||
host: {
|
||||
'role': 'checkbox',
|
||||
'[id]': 'id',
|
||||
'[class.md-checkbox-indeterminate]': 'indeterminate',
|
||||
'[class.md-checkbox-checked]': 'checked',
|
||||
'[class.md-checkbox-disabled]': 'disabled',
|
||||
'[class.md-checkbox-align-end]': 'align == "end"',
|
||||
'[attr.tabindex]': 'disabled ? null : tabindex',
|
||||
'[attr.aria-label]': 'ariaLabel',
|
||||
'[attr.aria-labelledby]': 'labelId',
|
||||
'[attr.aria-checked]': 'getAriaChecked()',
|
||||
'[attr.aria-disabled]': 'disabled',
|
||||
'(click)': 'onInteractionEvent($event)',
|
||||
'(keydown.space)': 'onSpaceDown($event)',
|
||||
'(keyup.space)': 'onInteractionEvent($event)',
|
||||
'(blur)': 'onTouched()'
|
||||
'[class.md-checkbox-focused]': 'hasFocus',
|
||||
},
|
||||
providers: [MD_CHECKBOX_CONTROL_VALUE_ACCESSOR],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
@@ -86,9 +74,19 @@ export class MdCheckbox implements ControlValueAccessor {
|
||||
*/
|
||||
@Input('aria-label') ariaLabel: string = '';
|
||||
|
||||
/**
|
||||
* Users can specify the `aria-labelledby` attribute which will be forwarded to the input element
|
||||
*/
|
||||
@Input('aria-labelledby') ariaLabelledby: string = null;
|
||||
|
||||
/** A unique id for the checkbox. If one is not supplied, it is auto-generated. */
|
||||
@Input() id: string = `md-checkbox-${++nextId}`;
|
||||
|
||||
/** ID to be applied to the `input` element */
|
||||
get inputId(): string {
|
||||
return `input-${this.id}`;
|
||||
}
|
||||
|
||||
/** Whether or not the checkbox should come before or after the label. */
|
||||
@Input() align: 'start' | 'end' = 'start';
|
||||
|
||||
@@ -104,6 +102,9 @@ export class MdCheckbox implements ControlValueAccessor {
|
||||
*/
|
||||
@Input() tabindex: number = 0;
|
||||
|
||||
/** Name value will be applied to the input element if present */
|
||||
@Input() name: string = null;
|
||||
|
||||
/** Event emitted when the checkbox's `checked` value changes. */
|
||||
@Output() change: EventEmitter<boolean> = new EventEmitter<boolean>();
|
||||
|
||||
@@ -123,6 +124,8 @@ export class MdCheckbox implements ControlValueAccessor {
|
||||
|
||||
private _changeSubscription: {unsubscribe: () => any} = null;
|
||||
|
||||
hasFocus: boolean = false;
|
||||
|
||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||
|
||||
/**
|
||||
@@ -172,43 +175,6 @@ export class MdCheckbox implements ControlValueAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
/** The id that is attached to the checkbox's label. */
|
||||
get labelId() {
|
||||
return `${this.id}-label`;
|
||||
}
|
||||
|
||||
/** Returns the proper aria-checked attribute value based on the checkbox's state. */
|
||||
getAriaChecked() {
|
||||
if (this.indeterminate) {
|
||||
return 'mixed';
|
||||
}
|
||||
return this.checked ? 'true' : 'false';
|
||||
}
|
||||
|
||||
/** Toggles the checked state of the checkbox. If the checkbox is disabled, this does nothing. */
|
||||
toggle() {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler used for both (click) and (keyup.space) events. Delegates to toggle().
|
||||
*/
|
||||
onInteractionEvent(event: Event) {
|
||||
if (this.disabled) {
|
||||
event.stopPropagation();
|
||||
return;
|
||||
}
|
||||
this.toggle();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/** Implemented as part of ControlValueAccessor. */
|
||||
writeValue(value: any) {
|
||||
this.checked = !!value;
|
||||
@@ -248,6 +214,43 @@ export class MdCheckbox implements ControlValueAccessor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the component when the input has focus so that we can style accordingly
|
||||
* @internal
|
||||
*/
|
||||
onInputFocus() {
|
||||
this.hasFocus = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Informs the component when we lose focus in order to style accordingly
|
||||
* @internal
|
||||
*/
|
||||
onInputBlur() {
|
||||
this.hasFocus = false;
|
||||
this.onTouched();
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the `checked` value between true and false
|
||||
*/
|
||||
toggle() {
|
||||
this.checked = !this.checked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for checkbox input element. Toggles checked state if element is not disabled.
|
||||
* @param event
|
||||
* @internal
|
||||
*/
|
||||
onInteractionEvent(event: Event) {
|
||||
if (this.disabled) {
|
||||
event.stopPropagation();
|
||||
} else {
|
||||
this.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
private _getAnimationClassForCheckStateTransition(
|
||||
oldState: TransitionCheckState, newState: TransitionCheckState): string {
|
||||
var animSuffix: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "default-theme";
|
||||
@import "variables";
|
||||
@import "mixins";
|
||||
|
||||
$md-radio-size: $md-toggle-size !default;
|
||||
|
||||
@@ -26,34 +27,6 @@ md-radio-button {
|
||||
width: $md-radio-size;
|
||||
}
|
||||
|
||||
// TODO(mtlin): Replace when ink ripple component is implemented.
|
||||
// A placeholder ink ripple, shown when keyboard focused.
|
||||
.md-ink-ripple {
|
||||
background-color: md-color($md-accent);
|
||||
border-radius: 50%;
|
||||
height: 48px;
|
||||
left: 10px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
transform: translate(-50%,-50%);
|
||||
transition: opacity ease 0.28s, background-color ease 0.28s;
|
||||
width: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
// Fade in when radio focused.
|
||||
.md-radio-focused & {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
// TODO(mtlin): This corresponds to disabled focus state, but it's unclear how to enter into
|
||||
// this state.
|
||||
.md-radio-disabled & {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
|
||||
// The outer circle for the radio, always present.
|
||||
.md-radio-outer-circle {
|
||||
border-color: md-color($md-foreground, secondary-text);
|
||||
@@ -117,17 +90,12 @@ md-radio-button {
|
||||
// Underlying native input element.
|
||||
// Visually hidden but still able to respond to focus.
|
||||
.md-radio-input {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
opacity: 0;
|
||||
appearance: none;
|
||||
border: none;
|
||||
@include md-visually-hidden;
|
||||
}
|
||||
|
||||
// Basic disabled state.
|
||||
.md-radio-disabled, .md-radio-disabled .md-radio-label {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@include md-temporary-ink-ripple(radio);
|
||||
|
||||
@@ -24,4 +24,34 @@
|
||||
position: absolute;
|
||||
text-transform: none;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
@mixin md-temporary-ink-ripple($component) {
|
||||
// TODO(mtlin): Replace when ink ripple component is implemented.
|
||||
// A placeholder ink ripple, shown when keyboard focused.
|
||||
.md-ink-ripple {
|
||||
background-color: md-color($md-accent);
|
||||
border-radius: 50%;
|
||||
height: 48px;
|
||||
left: 50%;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(-50%,-50%);
|
||||
transition: opacity ease 0.28s, background-color ease 0.28s;
|
||||
width: 48px;
|
||||
|
||||
// Fade in when radio focused.
|
||||
.md-#{$component}-focused & {
|
||||
opacity: 0.1;
|
||||
}
|
||||
|
||||
// TODO(mtlin): This corresponds to disabled focus state, but it's unclear how to enter into
|
||||
// this state.
|
||||
.md-#{$component}-disabled & {
|
||||
background: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,8 +40,11 @@ class MdCheckboxDemoNestedChecklist {
|
||||
}
|
||||
];
|
||||
|
||||
allComplete(tasks: Task[]): boolean {
|
||||
return tasks.every(t => t.completed);
|
||||
allComplete(task: Task): boolean {
|
||||
let subtasks = task.subtasks;
|
||||
return subtasks.every(t => t.completed) ? true
|
||||
: subtasks.every(t => !t.completed) ? false
|
||||
: task.completed;
|
||||
}
|
||||
|
||||
someComplete(tasks: Task[]): boolean {
|
||||
@@ -52,10 +55,6 @@ class MdCheckboxDemoNestedChecklist {
|
||||
setAllCompleted(tasks: Task[], completed: boolean) {
|
||||
tasks.forEach(t => t.completed = completed);
|
||||
}
|
||||
|
||||
updateOnSubtaskChange(task: Task) {
|
||||
task.completed = this.allComplete(task.subtasks);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<ul>
|
||||
<li *ngFor="let task of tasks">
|
||||
<md-checkbox [(ngModel)]="task.completed"
|
||||
[checked]="allComplete(task.subtasks)"
|
||||
[checked]="allComplete(task)"
|
||||
[indeterminate]="someComplete(task.subtasks)"
|
||||
(change)="setAllCompleted(task.subtasks, $event)">
|
||||
<h3>{{task.name}}</h3>
|
||||
|
||||
Reference in New Issue
Block a user