diff --git a/src/lib/checkbox/checkbox-config.ts b/src/lib/checkbox/checkbox-config.ts new file mode 100644 index 000000000000..a783634b31c8 --- /dev/null +++ b/src/lib/checkbox/checkbox-config.ts @@ -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('mat-checkbox-click-action'); diff --git a/src/lib/checkbox/checkbox.md b/src/lib/checkbox/checkbox.md index 0ae96cf7939e..8c4f25543877 100644 --- a/src/lib/checkbox/checkbox.md +++ b/src/lib/checkbox/checkbox.md @@ -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 ``. + ### Theming The color of a `` 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'`. diff --git a/src/lib/checkbox/checkbox.spec.ts b/src/lib/checkbox/checkbox.spec.ts index 62c67dafd841..b03514d6b122 100644 --- a/src/lib/checkbox/checkbox.spec.ts +++ b/src/lib/checkbox/checkbox.spec.ts @@ -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', () => { @@ -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', () => { diff --git a/src/lib/checkbox/checkbox.ts b/src/lib/checkbox/checkbox.ts index 2fa3dd17daf3..7131c307da47 100644 --- a/src/lib/checkbox/checkbox.ts +++ b/src/lib/checkbox/checkbox.ts @@ -17,8 +17,10 @@ import { ElementRef, EventEmitter, forwardRef, + Inject, Input, OnDestroy, + Optional, Output, ViewChild, ViewEncapsulation, @@ -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. @@ -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; } @@ -369,10 +375,13 @@ 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); }); @@ -380,12 +389,17 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc 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; } } diff --git a/src/lib/checkbox/public-api.ts b/src/lib/checkbox/public-api.ts index e93c91626f04..ec37deb2d32c 100644 --- a/src/lib/checkbox/public-api.ts +++ b/src/lib/checkbox/public-api.ts @@ -7,6 +7,7 @@ */ export * from './checkbox'; +export * from './checkbox-config'; export * from './checkbox-module'; export * from './checkbox-required-validator';