Skip to content
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(material/checkbox): add the ability to interact with disabled checkboxes #29474

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/dev-app/checkbox/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ng_module(
"//src/material/form-field",
"//src/material/input",
"//src/material/select",
"//src/material/tooltip",
"@npm//@angular/forms",
],
)
Expand Down
11 changes: 6 additions & 5 deletions src/dev-app/checkbox/checkbox-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,20 @@ <h1>mat-checkbox: Basic Example</h1>
(change)="isIndeterminate = false"
[indeterminate]="isIndeterminate"
[disabled]="isDisabled"
[labelPosition]="labelPosition">
[disabledInteractive]="isDisabledInteractive"
[labelPosition]="labelPosition"
[matTooltip]="isDisabled ? 'Tooltip that only shows up when disabled' : null">
Do you want to <em>foobar</em> the <em>bazquux</em>?

</mat-checkbox> - <strong>{{printResult()}}</strong>
</form>
<div class="demo-checkbox">
<input id="indeterminate-toggle"
type="checkbox"
[(ngModel)]="isIndeterminate"
[disabled]="isDisabled">
<input id="indeterminate-toggle" type="checkbox" [(ngModel)]="isIndeterminate">
<label for="indeterminate-toggle">Toggle Indeterminate</label>
<input id="disabled-toggle" type="checkbox" [(ngModel)]="isDisabled">
<label for="disabled-toggle">Toggle Disabled</label>
<input id="disabled-interactive-toggle" type="checkbox" [(ngModel)]="isDisabledInteractive">
<label for="disabled-interactive-toggle">Toggle Disabled Interactive</label>
<input id="color-toggle" type="checkbox" [(ngModel)]="useAlternativeColor">
<label for="color-toggle">Toggle Color</label>
</div>
Expand Down
11 changes: 7 additions & 4 deletions src/dev-app/checkbox/checkbox-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {MAT_CHECKBOX_DEFAULT_OPTIONS, MatCheckboxModule} from '@angular/material
import {MatPseudoCheckboxModule, ThemePalette} from '@angular/material/core';
import {MatInputModule} from '@angular/material/input';
import {MatSelectModule} from '@angular/material/select';
import {MatTooltip} from '@angular/material/tooltip';

export interface Task {
name: string;
Expand Down Expand Up @@ -114,15 +115,17 @@ export class MatCheckboxDemoNestedChecklist {
ClickActionNoop,
ClickActionCheck,
AnimationsNoop,
MatTooltip,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CheckboxDemo {
isIndeterminate: boolean = false;
isChecked: boolean = false;
isDisabled: boolean = false;
isIndeterminate = false;
isChecked = false;
isDisabled = false;
isDisabledInteractive = false;
labelPosition: 'before' | 'after' = 'after';
useAlternativeColor: boolean = false;
useAlternativeColor = false;

demoRequired = false;
demoLabelAfter = false;
Expand Down
42 changes: 38 additions & 4 deletions src/material/checkbox/_checkbox-common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,21 @@ $_fallback-size: 40px;
@include token-utils.create-token-slot(border-color, selected-focus-icon-color);
@include token-utils.create-token-slot(background-color, selected-focus-icon-color);
}

// Needs extra specificity to override the focus, hover, active states.
.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive {
.mdc-checkbox:hover .mdc-checkbox__native-control ~ .mdc-checkbox__background,
.mdc-checkbox .mdc-checkbox__native-control:focus ~ .mdc-checkbox__background,
.mdc-checkbox__background {
@include token-utils.create-token-slot(border-color, disabled-unselected-icon-color);
}

.mdc-checkbox__native-control:checked ~ .mdc-checkbox__background,
.mdc-checkbox__native-control:indeterminate ~ .mdc-checkbox__background {
@include token-utils.create-token-slot(background-color, disabled-selected-icon-color);
border-color: transparent;
}
}
}

.mdc-checkbox__checkmark {
Expand All @@ -158,8 +173,12 @@ $_fallback-size: 40px;
}

@include token-utils.use-tokens($prefix, $slots) {
.mdc-checkbox--disabled .mdc-checkbox__checkmark {
@include token-utils.create-token-slot(color, disabled-selected-checkmark-color);
.mdc-checkbox--disabled {
&, &.mat-mdc-checkbox-disabled-interactive {
.mdc-checkbox__checkmark {
@include token-utils.create-token-slot(color, disabled-selected-checkmark-color);
}
}
}
}

Expand Down Expand Up @@ -193,8 +212,12 @@ $_fallback-size: 40px;
}

@include token-utils.use-tokens($prefix, $slots) {
.mdc-checkbox--disabled .mdc-checkbox__mixedmark {
@include token-utils.create-token-slot(border-color, disabled-selected-checkmark-color);
.mdc-checkbox--disabled {
&, &.mat-mdc-checkbox-disabled-interactive {
.mdc-checkbox__mixedmark {
@include token-utils.create-token-slot(border-color, disabled-selected-checkmark-color);
}
}
}
}

Expand Down Expand Up @@ -520,4 +543,15 @@ $_fallback-size: 40px;
);
}
}

// Needs extra specificity to override the focus, hover, active states.
.mdc-checkbox--disabled.mat-mdc-checkbox-disabled-interactive & {
.mdc-checkbox__native-control ~ .mat-mdc-checkbox-ripple .mat-ripple-element,
.mdc-checkbox__native-control ~ .mdc-checkbox__ripple {
@include token-utils.create-token-slot(
background-color,
unselected-hover-state-layer-color
);
}
}
}
5 changes: 5 additions & 0 deletions src/material/checkbox/checkbox-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ export interface MatCheckboxDefaultOptions {
* https://material.angular.io/guide/theming#using-component-color-variants
*/
color?: ThemePalette;

/** Default checkbox click action for checkboxes. */
clickAction?: MatCheckboxClickAction;

/** Whether disabled checkboxes should be interactive. */
disabledInteractive?: boolean;
}

/** Injection token to be used to override the default options for `mat-checkbox`. */
Expand All @@ -36,6 +40,7 @@ export function MAT_CHECKBOX_DEFAULT_OPTIONS_FACTORY(): MatCheckboxDefaultOption
return {
color: 'accent',
clickAction: 'check-indeterminate',
disabledInteractive: false,
};
}

Expand Down
9 changes: 4 additions & 5 deletions src/material/checkbox/checkbox.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
[attr.aria-labelledby]="ariaLabelledby"
[attr.aria-describedby]="ariaDescribedby"
[attr.aria-checked]="indeterminate ? 'mixed' : null"
[attr.aria-disabled]="disabled && disabledInteractive ? true : null"
[attr.name]="name"
[attr.value]="value"
[checked]="checked"
[indeterminate]="indeterminate"
[disabled]="disabled"
[disabled]="disabled && !disabledInteractive"
[id]="inputId"
[required]="required"
[tabIndex]="disabled ? -1 : tabIndex"
[tabIndex]="disabled && !disabledInteractive ? -1 : tabIndex"
(blur)="_onBlur()"
(click)="_onInputClick()"
(change)="_onInteractionEvent($event)"/>
Expand All @@ -43,9 +44,7 @@
(#14385). Putting a click handler on the <label/> caused this bug because the browser produced
an unnecessary accessibility tree node.
-->
<label class="mdc-label"
#label
[for]="inputId">
<label class="mdc-label" #label [for]="inputId">
<ng-content></ng-content>
</label>
</div>
24 changes: 17 additions & 7 deletions src/material/checkbox/checkbox.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,24 @@
}
}

&.mat-mdc-checkbox-disabled label {
cursor: default;
&.mat-mdc-checkbox-disabled {
&.mat-mdc-checkbox-disabled-interactive {
pointer-events: auto;

@include token-utils.use-tokens(
tokens-mat-checkbox.$prefix,
tokens-mat-checkbox.get-token-slots()
) {
@include token-utils.create-token-slot(color, disabled-label-color);
input {
cursor: default;
}
}

label {
cursor: default;

@include token-utils.use-tokens(
tokens-mat-checkbox.$prefix,
tokens-mat-checkbox.get-token-slots()
) {
@include token-utils.create-token-slot(color, disabled-label-color);
}
}
}

Expand Down
44 changes: 34 additions & 10 deletions src/material/checkbox/checkbox.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,28 @@ describe('MDC-based MatCheckbox', () => {
expect(checkboxNativeElement.querySelector('svg')!.getAttribute('focusable')).toBe('false');
}));

it('should be able to mark a checkbox as disabled while keeping it interactive', fakeAsync(() => {
testComponent.isDisabled = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();

expect(checkboxNativeElement.classList).not.toContain(
'mat-mdc-checkbox-disabled-interactive',
);
expect(inputElement.hasAttribute('aria-disabled')).toBe(false);
expect(inputElement.tabIndex).toBe(-1);
expect(inputElement.disabled).toBe(true);

testComponent.disabledInteractive = true;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();

expect(checkboxNativeElement.classList).toContain('mat-mdc-checkbox-disabled-interactive');
expect(inputElement.getAttribute('aria-disabled')).toBe('true');
expect(inputElement.tabIndex).toBe(0);
expect(inputElement.disabled).toBe(false);
}));

describe('ripple elements', () => {
it('should show ripples on label mousedown', fakeAsync(() => {
const rippleSelector = '.mat-ripple-element:not(.mat-checkbox-persistent-ripple)';
Expand Down Expand Up @@ -1111,6 +1133,7 @@ describe('MatCheckboxDefaultOptions', () => {
[color]="checkboxColor"
[disableRipple]="disableRipple"
[value]="checkboxValue"
[disabledInteractive]="disabledInteractive"
(change)="onCheckboxChange($event)">
Simple checkbox
</mat-checkbox>
Expand All @@ -1120,13 +1143,14 @@ describe('MatCheckboxDefaultOptions', () => {
})
class SingleCheckbox {
labelPos: 'before' | 'after' = 'after';
isChecked: boolean = false;
isRequired: boolean = false;
isIndeterminate: boolean = false;
isDisabled: boolean = false;
disableRipple: boolean = false;
parentElementClicked: boolean = false;
parentElementKeyedUp: boolean = false;
isChecked = false;
isRequired = false;
isIndeterminate = false;
isDisabled = false;
disableRipple = false;
parentElementClicked = false;
parentElementKeyedUp = false;
disabledInteractive = false;
checkboxId: string | null = 'simple-check';
checkboxColor: ThemePalette = 'primary';
checkboxValue: string = 'single_checkbox';
Expand All @@ -1143,9 +1167,9 @@ class SingleCheckbox {
imports: [MatCheckbox, FormsModule],
})
class CheckboxWithNgModel {
isGood: boolean = false;
isRequired: boolean = true;
isDisabled: boolean = false;
isGood = false;
isRequired = true;
isDisabled = false;
}

@Component({
Expand Down
11 changes: 10 additions & 1 deletion src/material/checkbox/checkbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ const defaults = MAT_CHECKBOX_DEFAULT_OPTIONS_FACTORY();
// Add classes that users can use to more easily target disabled or checked checkboxes.
'[class.mat-mdc-checkbox-disabled]': 'disabled',
'[class.mat-mdc-checkbox-checked]': 'checked',
'[class.mat-mdc-checkbox-disabled-interactive]': 'disabledInteractive',
'[class]': 'color ? "mat-" + color : "mat-accent"',
},
providers: [
Expand Down Expand Up @@ -211,6 +212,10 @@ export class MatCheckbox
*/
@Input() color: string | undefined;

/** Whether the checkbox should remain interactive when it is disabled. */
@Input({transform: booleanAttribute})
disabledInteractive: boolean;

/**
* Reference to the MatRipple instance of the checkbox.
* @deprecated Considered an implementation detail. To be removed.
Expand Down Expand Up @@ -241,6 +246,7 @@ export class MatCheckbox
this.color = this._options.color || defaults.color;
this.tabIndex = parseInt(tabIndex) || 0;
this.id = this._uniqueId = `mat-mdc-checkbox-${++nextUniqueId}`;
this.disabledInteractive = _options?.disabledInteractive ?? false;
}

ngOnChanges(changes: SimpleChanges) {
Expand Down Expand Up @@ -422,7 +428,10 @@ export class MatCheckbox
// 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 && clickAction === 'noop') {
} else if (
(this.disabled && this.disabledInteractive) ||
(!this.disabled && 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;
Expand Down
6 changes: 5 additions & 1 deletion tools/public_api_guard/material/checkbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccess
protected _createChangeEvent(isChecked: boolean): MatCheckboxChange;
get disabled(): boolean;
set disabled(value: boolean);
disabledInteractive: boolean;
disableRipple: boolean;
// (undocumented)
_elementRef: ElementRef<HTMLElement>;
Expand All @@ -82,6 +83,8 @@ export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccess
// (undocumented)
static ngAcceptInputType_disabled: unknown;
// (undocumented)
static ngAcceptInputType_disabledInteractive: unknown;
// (undocumented)
static ngAcceptInputType_disableRipple: unknown;
// (undocumented)
static ngAcceptInputType_indeterminate: unknown;
Expand Down Expand Up @@ -123,7 +126,7 @@ export class MatCheckbox implements AfterViewInit, OnChanges, ControlValueAccess
// (undocumented)
writeValue(value: any): void;
// (undocumented)
static ɵcmp: i0.ɵɵComponentDeclaration<MatCheckbox, "mat-checkbox", ["matCheckbox"], { "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "ariaDescribedby": { "alias": "aria-describedby"; "required": false; }; "id": { "alias": "id"; "required": false; }; "required": { "alias": "required"; "required": false; }; "labelPosition": { "alias": "labelPosition"; "required": false; }; "name": { "alias": "name"; "required": false; }; "value": { "alias": "value"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; "color": { "alias": "color"; "required": false; }; "checked": { "alias": "checked"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "indeterminate": { "alias": "indeterminate"; "required": false; }; }, { "change": "change"; "indeterminateChange": "indeterminateChange"; }, never, ["*"], true, never>;
static ɵcmp: i0.ɵɵComponentDeclaration<MatCheckbox, "mat-checkbox", ["matCheckbox"], { "ariaLabel": { "alias": "aria-label"; "required": false; }; "ariaLabelledby": { "alias": "aria-labelledby"; "required": false; }; "ariaDescribedby": { "alias": "aria-describedby"; "required": false; }; "id": { "alias": "id"; "required": false; }; "required": { "alias": "required"; "required": false; }; "labelPosition": { "alias": "labelPosition"; "required": false; }; "name": { "alias": "name"; "required": false; }; "value": { "alias": "value"; "required": false; }; "disableRipple": { "alias": "disableRipple"; "required": false; }; "tabIndex": { "alias": "tabIndex"; "required": false; }; "color": { "alias": "color"; "required": false; }; "disabledInteractive": { "alias": "disabledInteractive"; "required": false; }; "checked": { "alias": "checked"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "indeterminate": { "alias": "indeterminate"; "required": false; }; }, { "change": "change"; "indeterminateChange": "indeterminateChange"; }, never, ["*"], true, never>;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<MatCheckbox, [null, null, null, { attribute: "tabindex"; }, { optional: true; }, { optional: true; }]>;
}
Expand All @@ -141,6 +144,7 @@ export type MatCheckboxClickAction = 'noop' | 'check' | 'check-indeterminate' |
export interface MatCheckboxDefaultOptions {
clickAction?: MatCheckboxClickAction;
color?: ThemePalette;
disabledInteractive?: boolean;
}

// @public (undocumented)
Expand Down
Loading