Skip to content

Commit

Permalink
feat(checkbox): Support checkbox click action config (#8521)
Browse files Browse the repository at this point in the history
  • Loading branch information
tinayuangao authored Dec 1, 2017
1 parent f41fa8c commit 537b8b5
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 4 deletions.
24 changes: 24 additions & 0 deletions src/lib/checkbox/checkbox-config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {InjectionToken} from '@angular/core';


/**
* Checkbox click action when user click on input element.
* noop: Do not toggle checked or indeterminate.
* check: Only toggle checked status, ignore indeterminate.
* check-indeterminate: Toggle checked status, set indeterminate to false. Default behavior.
* undefined: Same as `check-indeterminate`.
*/
export type MatCheckboxClickAction = 'noop' | 'check' | 'check-indeterminate' | undefined;

/**
* Injection token that can be used to specify the checkbox click behavior.
*/
export const MAT_CHECKBOX_CLICK_ACTION =
new InjectionToken<MatCheckboxClickAction>('mat-checkbox-click-action');
19 changes: 19 additions & 0 deletions src/lib/checkbox/checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,25 @@ While the `indeterminate` property of the checkbox is true, it will render as in
regardless of the `checked` value. Any interaction with the checkbox by a user (i.e., clicking) will
remove the indeterminate state.

### Click action config
When user clicks on the `mat-checkbox`, the default behavior is toggle `checked` value and set
`indeterminate` to `false`. Developers now are able to change the behavior by providing a new value
of `MatCheckboxClickAction` to the checkbox. The possible values are:

#### `noop`
Do not change the `checked` value or `indeterminate` value. Developers have the power to
implement customized click actions.

#### `check`
Toggle `checked` value of the checkbox, ignore `indeterminate` value. If the
checkbox is in `indeterminate` state, the checkbox will display as an `indeterminate` checkbox
regardless the `checked` value.

####`check-indeterminate`
Default behavior of `mat-checkbox`. Always set `indeterminate` to `false`
when user click on the `mat-checkbox`.
This matches the behavior of native `<input type="checkbox">`.

### Theming
The color of a `<mat-checkbox>` can be changed by using the `color` property. By default, checkboxes
use the theme's accent color. This can be changed to `'primary'` or `'warn'`.
Expand Down
107 changes: 107 additions & 0 deletions src/lib/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {By} from '@angular/platform-browser';
import {dispatchFakeEvent} from '@angular/cdk/testing';
import {MatCheckbox, MatCheckboxChange, MatCheckboxModule} from './index';
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '@angular/material/core';
import {MAT_CHECKBOX_CLICK_ACTION} from './checkbox-config';


describe('MatCheckbox', () => {
Expand Down Expand Up @@ -544,6 +545,112 @@ describe('MatCheckbox', () => {
expect(checkboxNativeElement).not.toMatch(/^mat\-checkbox\-anim/g);
});
});

describe(`when MAT_CHECKBOX_CLICK_ACTION is 'check'`, () => {
beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule],
declarations: [
SingleCheckbox,
],
providers: [
{provide: MAT_CHECKBOX_CLICK_ACTION, useValue: 'check'}
]
});

fixture = TestBed.createComponent(SingleCheckbox);
fixture.detectChanges();

checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox));
checkboxNativeElement = checkboxDebugElement.nativeElement;
checkboxInstance = checkboxDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;

inputElement = checkboxNativeElement.querySelector('input') as HTMLInputElement;
labelElement = checkboxNativeElement.querySelector('label') as HTMLLabelElement;
});

it('should not set `indeterminate` to false on click if check is set', fakeAsync(() => {
testComponent.isIndeterminate = true;
inputElement.click();

fixture.detectChanges();
flushMicrotasks();
fixture.detectChanges();
expect(inputElement.checked).toBe(true);
expect(checkboxNativeElement.classList).toContain('mat-checkbox-checked');
expect(inputElement.indeterminate).toBe(true);
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');
}));
});

describe(`when MAT_CHECKBOX_CLICK_ACTION is 'noop'`, () => {
beforeEach(() => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule],
declarations: [
SingleCheckbox,
],
providers: [
{provide: MAT_CHECKBOX_CLICK_ACTION, useValue: 'noop'}
]
});

fixture = TestBed.createComponent(SingleCheckbox);
fixture.detectChanges();

checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox));
checkboxNativeElement = checkboxDebugElement.nativeElement;
checkboxInstance = checkboxDebugElement.componentInstance;
testComponent = fixture.debugElement.componentInstance;
inputElement = checkboxNativeElement.querySelector('input') as HTMLInputElement;
labelElement = checkboxNativeElement.querySelector('label') as HTMLLabelElement;
});

it('should not change `indeterminate` on click if noop is set', fakeAsync(() => {
testComponent.isIndeterminate = true;
inputElement.click();

fixture.detectChanges();
flushMicrotasks();
fixture.detectChanges();

expect(inputElement.checked).toBe(false);
expect(checkboxNativeElement.classList).not.toContain('mat-checkbox-checked');
expect(inputElement.indeterminate).toBe(true);
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');
}));


it(`should not change 'checked' or 'indeterminate' on click if noop is set`, fakeAsync(() => {
testComponent.isChecked = true;
testComponent.isIndeterminate = true;
inputElement.click();

fixture.detectChanges();
flushMicrotasks();
fixture.detectChanges();

expect(inputElement.checked).toBe(true);
expect(checkboxNativeElement.classList).toContain('mat-checkbox-checked');
expect(inputElement.indeterminate).toBe(true);
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');

testComponent.isChecked = false;
inputElement.click();

fixture.detectChanges();
flushMicrotasks();
fixture.detectChanges();

expect(inputElement.checked).toBe(false);
expect(checkboxNativeElement.classList).not.toContain('mat-checkbox-checked');
expect(inputElement.indeterminate).toBe(true, 'indeterminate should not change');
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');
}));
});
});

describe('with change event and no initial value', () => {
Expand Down
22 changes: 18 additions & 4 deletions src/lib/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
ElementRef,
EventEmitter,
forwardRef,
Inject,
Input,
OnDestroy,
Optional,
Output,
ViewChild,
ViewEncapsulation,
Expand All @@ -37,6 +39,7 @@ import {
RippleConfig,
RippleRef,
} from '@angular/material/core';
import {MAT_CHECKBOX_CLICK_ACTION, MatCheckboxClickAction} from './checkbox-config';


// Increasing integer for generating unique ids for checkbox components.
Expand Down Expand Up @@ -203,8 +206,11 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
constructor(elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
private _focusMonitor: FocusMonitor,
@Attribute('tabindex') tabIndex: string) {
@Attribute('tabindex') tabIndex: string,
@Optional() @Inject(MAT_CHECKBOX_CLICK_ACTION)
private _clickAction: MatCheckboxClickAction) {
super(elementRef);

this.tabIndex = parseInt(tabIndex) || 0;
}

Expand Down Expand Up @@ -369,23 +375,31 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
// Preventing bubbling for the second event will solve that issue.
event.stopPropagation();

if (!this.disabled) {
// If resetIndeterminate is false, and the current state is indeterminate, do nothing on click
if (!this.disabled && this._clickAction !== 'noop') {
// When user manually click on the checkbox, `indeterminate` is set to false.
if (this._indeterminate) {
if (this.indeterminate && this._clickAction !== 'check') {

Promise.resolve().then(() => {
console.log(`reset indeterminate`);
this._indeterminate = false;
this.indeterminateChange.emit(this._indeterminate);
});
}

this.toggle();
this._transitionCheckState(
this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);

// Emit our custom change event if the native input emitted one.
// It is important to only emit it, if the native input triggered one, because
// we don't want to trigger a change event, when the `checked` variable changes for example.
this._emitChangeEvent();
} else if (!this.disabled && this._clickAction === 'noop') {
// Reset native input when clicked with noop. The native checkbox becomes checked after
// click, reset it to be align with `checked` value of `mat-checkbox`.
this._inputElement.nativeElement.checked = this.checked;
this._inputElement.nativeElement.indeterminate = this.indeterminate;
}
}

Expand Down
1 change: 1 addition & 0 deletions src/lib/checkbox/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

export * from './checkbox';
export * from './checkbox-config';
export * from './checkbox-module';
export * from './checkbox-required-validator';

0 comments on commit 537b8b5

Please sign in to comment.