Skip to content

Commit 537b8b5

Browse files
authored
feat(checkbox): Support checkbox click action config (#8521)
1 parent f41fa8c commit 537b8b5

File tree

5 files changed

+169
-4
lines changed

5 files changed

+169
-4
lines changed

src/lib/checkbox/checkbox-config.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
import {InjectionToken} from '@angular/core';
9+
10+
11+
/**
12+
* Checkbox click action when user click on input element.
13+
* noop: Do not toggle checked or indeterminate.
14+
* check: Only toggle checked status, ignore indeterminate.
15+
* check-indeterminate: Toggle checked status, set indeterminate to false. Default behavior.
16+
* undefined: Same as `check-indeterminate`.
17+
*/
18+
export type MatCheckboxClickAction = 'noop' | 'check' | 'check-indeterminate' | undefined;
19+
20+
/**
21+
* Injection token that can be used to specify the checkbox click behavior.
22+
*/
23+
export const MAT_CHECKBOX_CLICK_ACTION =
24+
new InjectionToken<MatCheckboxClickAction>('mat-checkbox-click-action');

src/lib/checkbox/checkbox.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,25 @@ While the `indeterminate` property of the checkbox is true, it will render as in
2323
regardless of the `checked` value. Any interaction with the checkbox by a user (i.e., clicking) will
2424
remove the indeterminate state.
2525

26+
### Click action config
27+
When user clicks on the `mat-checkbox`, the default behavior is toggle `checked` value and set
28+
`indeterminate` to `false`. Developers now are able to change the behavior by providing a new value
29+
of `MatCheckboxClickAction` to the checkbox. The possible values are:
30+
31+
#### `noop`
32+
Do not change the `checked` value or `indeterminate` value. Developers have the power to
33+
implement customized click actions.
34+
35+
#### `check`
36+
Toggle `checked` value of the checkbox, ignore `indeterminate` value. If the
37+
checkbox is in `indeterminate` state, the checkbox will display as an `indeterminate` checkbox
38+
regardless the `checked` value.
39+
40+
####`check-indeterminate`
41+
Default behavior of `mat-checkbox`. Always set `indeterminate` to `false`
42+
when user click on the `mat-checkbox`.
43+
This matches the behavior of native `<input type="checkbox">`.
44+
2645
### Theming
2746
The color of a `<mat-checkbox>` can be changed by using the `color` property. By default, checkboxes
2847
use the theme's accent color. This can be changed to `'primary'` or `'warn'`.

src/lib/checkbox/checkbox.spec.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {By} from '@angular/platform-browser';
1212
import {dispatchFakeEvent} from '@angular/cdk/testing';
1313
import {MatCheckbox, MatCheckboxChange, MatCheckboxModule} from './index';
1414
import {RIPPLE_FADE_IN_DURATION, RIPPLE_FADE_OUT_DURATION} from '@angular/material/core';
15+
import {MAT_CHECKBOX_CLICK_ACTION} from './checkbox-config';
1516

1617

1718
describe('MatCheckbox', () => {
@@ -544,6 +545,112 @@ describe('MatCheckbox', () => {
544545
expect(checkboxNativeElement).not.toMatch(/^mat\-checkbox\-anim/g);
545546
});
546547
});
548+
549+
describe(`when MAT_CHECKBOX_CLICK_ACTION is 'check'`, () => {
550+
beforeEach(() => {
551+
TestBed.resetTestingModule();
552+
TestBed.configureTestingModule({
553+
imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule],
554+
declarations: [
555+
SingleCheckbox,
556+
],
557+
providers: [
558+
{provide: MAT_CHECKBOX_CLICK_ACTION, useValue: 'check'}
559+
]
560+
});
561+
562+
fixture = TestBed.createComponent(SingleCheckbox);
563+
fixture.detectChanges();
564+
565+
checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox));
566+
checkboxNativeElement = checkboxDebugElement.nativeElement;
567+
checkboxInstance = checkboxDebugElement.componentInstance;
568+
testComponent = fixture.debugElement.componentInstance;
569+
570+
inputElement = checkboxNativeElement.querySelector('input') as HTMLInputElement;
571+
labelElement = checkboxNativeElement.querySelector('label') as HTMLLabelElement;
572+
});
573+
574+
it('should not set `indeterminate` to false on click if check is set', fakeAsync(() => {
575+
testComponent.isIndeterminate = true;
576+
inputElement.click();
577+
578+
fixture.detectChanges();
579+
flushMicrotasks();
580+
fixture.detectChanges();
581+
expect(inputElement.checked).toBe(true);
582+
expect(checkboxNativeElement.classList).toContain('mat-checkbox-checked');
583+
expect(inputElement.indeterminate).toBe(true);
584+
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');
585+
}));
586+
});
587+
588+
describe(`when MAT_CHECKBOX_CLICK_ACTION is 'noop'`, () => {
589+
beforeEach(() => {
590+
TestBed.resetTestingModule();
591+
TestBed.configureTestingModule({
592+
imports: [MatCheckboxModule, FormsModule, ReactiveFormsModule],
593+
declarations: [
594+
SingleCheckbox,
595+
],
596+
providers: [
597+
{provide: MAT_CHECKBOX_CLICK_ACTION, useValue: 'noop'}
598+
]
599+
});
600+
601+
fixture = TestBed.createComponent(SingleCheckbox);
602+
fixture.detectChanges();
603+
604+
checkboxDebugElement = fixture.debugElement.query(By.directive(MatCheckbox));
605+
checkboxNativeElement = checkboxDebugElement.nativeElement;
606+
checkboxInstance = checkboxDebugElement.componentInstance;
607+
testComponent = fixture.debugElement.componentInstance;
608+
inputElement = checkboxNativeElement.querySelector('input') as HTMLInputElement;
609+
labelElement = checkboxNativeElement.querySelector('label') as HTMLLabelElement;
610+
});
611+
612+
it('should not change `indeterminate` on click if noop is set', fakeAsync(() => {
613+
testComponent.isIndeterminate = true;
614+
inputElement.click();
615+
616+
fixture.detectChanges();
617+
flushMicrotasks();
618+
fixture.detectChanges();
619+
620+
expect(inputElement.checked).toBe(false);
621+
expect(checkboxNativeElement.classList).not.toContain('mat-checkbox-checked');
622+
expect(inputElement.indeterminate).toBe(true);
623+
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');
624+
}));
625+
626+
627+
it(`should not change 'checked' or 'indeterminate' on click if noop is set`, fakeAsync(() => {
628+
testComponent.isChecked = true;
629+
testComponent.isIndeterminate = true;
630+
inputElement.click();
631+
632+
fixture.detectChanges();
633+
flushMicrotasks();
634+
fixture.detectChanges();
635+
636+
expect(inputElement.checked).toBe(true);
637+
expect(checkboxNativeElement.classList).toContain('mat-checkbox-checked');
638+
expect(inputElement.indeterminate).toBe(true);
639+
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');
640+
641+
testComponent.isChecked = false;
642+
inputElement.click();
643+
644+
fixture.detectChanges();
645+
flushMicrotasks();
646+
fixture.detectChanges();
647+
648+
expect(inputElement.checked).toBe(false);
649+
expect(checkboxNativeElement.classList).not.toContain('mat-checkbox-checked');
650+
expect(inputElement.indeterminate).toBe(true, 'indeterminate should not change');
651+
expect(checkboxNativeElement.classList).toContain('mat-checkbox-indeterminate');
652+
}));
653+
});
547654
});
548655

549656
describe('with change event and no initial value', () => {

src/lib/checkbox/checkbox.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import {
1717
ElementRef,
1818
EventEmitter,
1919
forwardRef,
20+
Inject,
2021
Input,
2122
OnDestroy,
23+
Optional,
2224
Output,
2325
ViewChild,
2426
ViewEncapsulation,
@@ -37,6 +39,7 @@ import {
3739
RippleConfig,
3840
RippleRef,
3941
} from '@angular/material/core';
42+
import {MAT_CHECKBOX_CLICK_ACTION, MatCheckboxClickAction} from './checkbox-config';
4043

4144

4245
// Increasing integer for generating unique ids for checkbox components.
@@ -203,8 +206,11 @@ export class MatCheckbox extends _MatCheckboxMixinBase implements ControlValueAc
203206
constructor(elementRef: ElementRef,
204207
private _changeDetectorRef: ChangeDetectorRef,
205208
private _focusMonitor: FocusMonitor,
206-
@Attribute('tabindex') tabIndex: string) {
209+
@Attribute('tabindex') tabIndex: string,
210+
@Optional() @Inject(MAT_CHECKBOX_CLICK_ACTION)
211+
private _clickAction: MatCheckboxClickAction) {
207212
super(elementRef);
213+
208214
this.tabIndex = parseInt(tabIndex) || 0;
209215
}
210216

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

372-
if (!this.disabled) {
378+
// If resetIndeterminate is false, and the current state is indeterminate, do nothing on click
379+
if (!this.disabled && this._clickAction !== 'noop') {
373380
// When user manually click on the checkbox, `indeterminate` is set to false.
374-
if (this._indeterminate) {
381+
if (this.indeterminate && this._clickAction !== 'check') {
382+
375383
Promise.resolve().then(() => {
384+
console.log(`reset indeterminate`);
376385
this._indeterminate = false;
377386
this.indeterminateChange.emit(this._indeterminate);
378387
});
379388
}
380389

381390
this.toggle();
382391
this._transitionCheckState(
383-
this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
392+
this._checked ? TransitionCheckState.Checked : TransitionCheckState.Unchecked);
384393

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

src/lib/checkbox/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
export * from './checkbox';
10+
export * from './checkbox-config';
1011
export * from './checkbox-module';
1112
export * from './checkbox-required-validator';
1213

0 commit comments

Comments
 (0)