-
Notifications
You must be signed in to change notification settings - Fork 6.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(checkbox): Support checkbox click action config #8521
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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'); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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'); | ||
})); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a test for not toggling |
||
|
||
|
||
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', () => { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,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') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is because the input element's |
||
// 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; | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mention that this matches the behavior of the native
<input type="checkbox">
?