Skip to content

Commit 33795a1

Browse files
authoredMar 20, 2025··
fix(material/chips): implement disabledInteractive in chip input (#30665)
Adds a `disabledInteractive` input to `MatChipInput`, similar to what we have in other components. I've also cleaned up the chips demo a bit.
1 parent aba4c44 commit 33795a1

File tree

10 files changed

+106
-45
lines changed

10 files changed

+106
-45
lines changed
 

Diff for: ‎goldens/material/chips/index.api.md

+9-1
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
252252
clear(): void;
253253
get disabled(): boolean;
254254
set disabled(value: boolean);
255+
disabledInteractive: boolean;
255256
// (undocumented)
256257
protected _elementRef: ElementRef<HTMLInputElement>;
257258
_emitChipEnd(event?: KeyboardEvent): void;
@@ -260,6 +261,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
260261
// (undocumented)
261262
_focus(): void;
262263
focused: boolean;
264+
protected _getReadonlyAttribute(): string | null;
263265
id: string;
264266
readonly inputElement: HTMLInputElement;
265267
_keydown(event: KeyboardEvent): void;
@@ -268,17 +270,22 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
268270
// (undocumented)
269271
static ngAcceptInputType_disabled: unknown;
270272
// (undocumented)
273+
static ngAcceptInputType_disabledInteractive: unknown;
274+
// (undocumented)
275+
static ngAcceptInputType_readonly: unknown;
276+
// (undocumented)
271277
ngOnChanges(): void;
272278
// (undocumented)
273279
ngOnDestroy(): void;
274280
// (undocumented)
275281
_onInput(): void;
276282
placeholder: string;
283+
readonly: boolean;
277284
separatorKeyCodes: readonly number[] | ReadonlySet<number>;
278285
// (undocumented)
279286
setDescribedByIds(ids: string[]): void;
280287
// (undocumented)
281-
static ɵdir: i0.ɵɵDirectiveDeclaration<MatChipInput, "input[matChipInputFor]", ["matChipInput", "matChipInputFor"], { "chipGrid": { "alias": "matChipInputFor"; "required": false; }; "addOnBlur": { "alias": "matChipInputAddOnBlur"; "required": false; }; "separatorKeyCodes": { "alias": "matChipInputSeparatorKeyCodes"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "id": { "alias": "id"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "chipEnd": "matChipInputTokenEnd"; }, never, never, true, never>;
288+
static ɵdir: i0.ɵɵDirectiveDeclaration<MatChipInput, "input[matChipInputFor]", ["matChipInput", "matChipInputFor"], { "chipGrid": { "alias": "matChipInputFor"; "required": false; }; "addOnBlur": { "alias": "matChipInputAddOnBlur"; "required": false; }; "separatorKeyCodes": { "alias": "matChipInputSeparatorKeyCodes"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "id": { "alias": "id"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "readonly": { "alias": "readonly"; "required": false; }; "disabledInteractive": { "alias": "matChipInputDisabledInteractive"; "required": false; }; }, { "chipEnd": "matChipInputTokenEnd"; }, never, never, true, never>;
282289
// (undocumented)
283290
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipInput, never>;
284291
}
@@ -433,6 +440,7 @@ export class MatChipRow extends MatChip implements AfterViewInit {
433440
// @public
434441
export interface MatChipsDefaultOptions {
435442
hideSingleSelectionIndicator?: boolean;
443+
inputDisabledInteractive?: boolean;
436444
separatorKeyCodes: readonly number[] | ReadonlySet<number>;
437445
}
438446

Diff for: ‎src/dev-app/chips/BUILD.bazel

-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ ng_module(
1717
"//src/material/core",
1818
"//src/material/form-field",
1919
"//src/material/icon",
20-
"//src/material/toolbar",
2120
],
2221
)
2322

Diff for: ‎src/dev-app/chips/chips-demo.html

+23-31
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div class="demo-chips">
22
<mat-card>
3-
<mat-toolbar color="primary">Static Chips</mat-toolbar>
3+
<mat-card-header>Static Chips</mat-card-header>
44

55
<mat-card-content>
66
<h4>Simple</h4>
@@ -111,26 +111,24 @@ <h4>With Events</h4>
111111
</mat-card>
112112

113113
<mat-card>
114-
<mat-toolbar color="primary">Selectable Chips</mat-toolbar>
114+
<mat-card-header>Chip Listbox</mat-card-header>
115115

116116
<mat-card-content>
117-
<button matButton (click)="disabledListboxes = !disabledListboxes">
118-
{{disabledListboxes ? "Enable" : "Disable"}}
119-
</button>
120-
<button matButton (click)="listboxesWithAvatar = !listboxesWithAvatar">
121-
{{listboxesWithAvatar ? "Hide Avatar" : "Show Avatar"}}
122-
</button>
117+
<p>Chip list utilizing the listbox pattern. Should be used for selectable chips.</p>
118+
119+
<mat-checkbox [(ngModel)]="disabledListboxes">Disabled</mat-checkbox>
120+
<mat-checkbox [(ngModel)]="listboxesWithAvatar">Show avatar</mat-checkbox>
123121

124122
<h4>Single selection</h4>
125123

126124
<mat-chip-listbox multiple="false" [disabled]="disabledListboxes">
127125
@for (shirtSize of shirtSizes; track shirtSize) {
128-
<mat-chip-option [disabled]="shirtSize.disabled">
129-
{{shirtSize.label}}
130-
@if (listboxesWithAvatar) {
131-
<mat-chip-avatar>{{shirtSize.avatar}}</mat-chip-avatar>
132-
}
133-
</mat-chip-option>
126+
<mat-chip-option [disabled]="shirtSize.disabled">
127+
{{shirtSize.label}}
128+
@if (listboxesWithAvatar) {
129+
<mat-chip-avatar>{{shirtSize.avatar}}</mat-chip-avatar>
130+
}
131+
</mat-chip-option>
134132
}
135133
</mat-chip-listbox>
136134

@@ -151,7 +149,7 @@ <h4>Multi selection</h4>
151149
</mat-card>
152150

153151
<mat-card>
154-
<mat-toolbar color="primary">Input Chips</mat-toolbar>
152+
<mat-card-header>Chip Grid</mat-card-header>
155153

156154
<mat-card-content>
157155
<p>
@@ -160,13 +158,9 @@ <h4>Multi selection</h4>
160158
They can be used inside a <code>&lt;mat-form-field&gt;</code>.
161159
</p>
162160

163-
<button matButton (click)="disableInputs = !disableInputs">
164-
{{disableInputs ? "Enable" : "Disable"}}
165-
</button>
166-
167-
<button matButton (click)="editable = !editable">
168-
{{editable ? "Disable editing" : "Enable editing"}}
169-
</button>
161+
<mat-checkbox [(ngModel)]="disableInputs">Disabled</mat-checkbox>
162+
<mat-checkbox [(ngModel)]="editable">Editable</mat-checkbox>
163+
<mat-checkbox [(ngModel)]="disabledInteractive">Disabled Interactive</mat-checkbox>
170164

171165
<h4>Input is last child of chip grid</h4>
172166

@@ -188,19 +182,15 @@ <h4>Input is last child of chip grid</h4>
188182
[matChipInputFor]="chipGrid1"
189183
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
190184
[matChipInputAddOnBlur]="addOnBlur"
185+
[matChipInputDisabledInteractive]="disabledInteractive"
191186
(matChipInputTokenEnd)="add($event)"
192-
aria-label="New contributor input..." />
187+
placeholder="Add a contributor"/>
193188
</mat-chip-grid>
194-
<input [disabled]="disableInputs"
195-
[matChipInputFor]="chipGrid1"
196-
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
197-
[matChipInputAddOnBlur]="addOnBlur"
198-
(matChipInputTokenEnd)="add($event)" />
199189
</mat-form-field>
200190

201191
<h4>Input is next sibling child of chip grid</h4>
202192

203-
<mat-form-field>
193+
<mat-form-field class="demo-has-chip-list">
204194
<mat-label>New Contributor...</mat-label>
205195
<mat-chip-grid #chipGrid2 [(ngModel)]="selectedPeople" required [disabled]="disableInputs">
206196
@for (person of people; track person) {
@@ -215,7 +205,9 @@ <h4>Input is next sibling child of chip grid</h4>
215205
<input [matChipInputFor]="chipGrid2"
216206
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
217207
[matChipInputAddOnBlur]="addOnBlur"
218-
(matChipInputTokenEnd)="add($event)"/>
208+
[matChipInputDisabledInteractive]="disabledInteractive"
209+
(matChipInputTokenEnd)="add($event)"
210+
placeholder="Add a contributor"/>
219211
</mat-form-field>
220212

221213
<p>
@@ -232,7 +224,7 @@ <h4>Options</h4>
232224
</mat-card>
233225

234226
<mat-card>
235-
<mat-toolbar color="primary">Miscellaneous</mat-toolbar>
227+
<mat-card-header>Miscellaneous</mat-card-header>
236228
<mat-card-content>
237229
<h4>Stacked</h4>
238230

Diff for: ‎src/dev-app/chips/chips-demo.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import {MatChipEditedEvent, MatChipInputEvent, MatChipsModule} from '@angular/ma
1717
import {ThemePalette} from '@angular/material/core';
1818
import {MatFormFieldModule} from '@angular/material/form-field';
1919
import {MatIconModule} from '@angular/material/icon';
20-
import {MatToolbarModule} from '@angular/material/toolbar';
2120

2221
export interface Person {
2322
name: string;
@@ -40,7 +39,6 @@ export interface DemoColor {
4039
MatChipsModule,
4140
MatFormFieldModule,
4241
MatIconModule,
43-
MatToolbarModule,
4442
ReactiveFormsModule,
4543
],
4644
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -54,6 +52,7 @@ export class ChipsDemo {
5452
listboxesWithAvatar = false;
5553
disableInputs = false;
5654
editable = false;
55+
disabledInteractive = false;
5756
message = '';
5857

5958
shirtSizes = [

Diff for: ‎src/material/chips/chip-input.spec.ts

+28-7
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,10 @@ import {
2222
} from './index';
2323

2424
describe('MatChipInput', () => {
25-
let fixture: ComponentFixture<any>;
25+
let fixture: ComponentFixture<TestChipInput>;
2626
let testChipInput: TestChipInput;
2727
let inputDebugElement: DebugElement;
28-
let inputNativeElement: HTMLElement;
28+
let inputNativeElement: HTMLInputElement;
2929
let chipInputDirective: MatChipInput;
3030
let dir = 'ltr';
3131

@@ -87,10 +87,28 @@ describe('MatChipInput', () => {
8787
fixture.changeDetectorRef.markForCheck();
8888
fixture.detectChanges();
8989

90-
expect(inputNativeElement.getAttribute('disabled')).toBe('true');
90+
expect(inputNativeElement.disabled).toBe(true);
9191
expect(chipInputDirective.disabled).toBe(true);
9292
});
9393

94+
it('should be able to set an input as being disabled and interactive', fakeAsync(() => {
95+
fixture.componentInstance.chipGridInstance.disabled = true;
96+
fixture.changeDetectorRef.markForCheck();
97+
fixture.detectChanges();
98+
99+
expect(inputNativeElement.disabled).toBe(true);
100+
expect(inputNativeElement.readOnly).toBe(false);
101+
expect(inputNativeElement.hasAttribute('aria-disabled')).toBe(false);
102+
103+
fixture.componentInstance.disabledInteractive = true;
104+
fixture.changeDetectorRef.markForCheck();
105+
fixture.detectChanges();
106+
107+
expect(inputNativeElement.disabled).toBe(false);
108+
expect(inputNativeElement.readOnly).toBe(true);
109+
expect(inputNativeElement.getAttribute('aria-disabled')).toBe('true');
110+
}));
111+
94112
it('should be aria-required if the list is required', () => {
95113
expect(inputNativeElement.hasAttribute('aria-required')).toBe(false);
96114

@@ -274,10 +292,12 @@ describe('MatChipInput', () => {
274292
<mat-form-field>
275293
<mat-chip-grid #chipGrid [required]="required">
276294
<mat-chip-row>Hello</mat-chip-row>
277-
<input [matChipInputFor]="chipGrid"
278-
[matChipInputAddOnBlur]="addOnBlur"
279-
(matChipInputTokenEnd)="add($event)"
280-
[placeholder]="placeholder" />
295+
<input
296+
[matChipInputFor]="chipGrid"
297+
[matChipInputAddOnBlur]="addOnBlur"
298+
[matChipInputDisabledInteractive]="disabledInteractive"
299+
(matChipInputTokenEnd)="add($event)"
300+
[placeholder]="placeholder" />
281301
</mat-chip-grid>
282302
</mat-form-field>
283303
`,
@@ -288,6 +308,7 @@ class TestChipInput {
288308
addOnBlur: boolean = false;
289309
placeholder = '';
290310
required = false;
311+
disabledInteractive = false;
291312

292313
add(_: MatChipInputEvent) {}
293314
}

Diff for: ‎src/material/chips/chip-input.ts

+17-1
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,12 @@ export interface MatChipInputEvent {
5757
'(focus)': '_focus()',
5858
'(input)': '_onInput()',
5959
'[id]': 'id',
60-
'[attr.disabled]': 'disabled || null',
60+
'[attr.disabled]': 'disabled && !disabledInteractive ? "" : null',
6161
'[attr.placeholder]': 'placeholder || null',
6262
'[attr.aria-invalid]': '_chipGrid && _chipGrid.ngControl ? _chipGrid.ngControl.invalid : null',
6363
'[attr.aria-required]': '_chipGrid && _chipGrid.required || null',
64+
'[attr.aria-disabled]': 'disabled && disabledInteractive ? "true" : null',
65+
'[attr.readonly]': '_getReadonlyAttribute()',
6466
'[attr.required]': '_chipGrid && _chipGrid.required || null',
6567
},
6668
})
@@ -117,6 +119,14 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
117119
}
118120
private _disabled: boolean = false;
119121

122+
/** Whether the input is readonly. */
123+
@Input({transform: booleanAttribute})
124+
readonly: boolean = false;
125+
126+
/** Whether the input should remain interactive when it is disabled. */
127+
@Input({alias: 'matChipInputDisabledInteractive', transform: booleanAttribute})
128+
disabledInteractive: boolean;
129+
120130
/** Whether the input is empty. */
121131
get empty(): boolean {
122132
return !this.inputElement.value;
@@ -133,6 +143,7 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
133143

134144
this.inputElement = this._elementRef.nativeElement as HTMLInputElement;
135145
this.separatorKeyCodes = defaultOptions.separatorKeyCodes;
146+
this.disabledInteractive = defaultOptions.inputDisabledInteractive ?? false;
136147

137148
if (formField) {
138149
this.inputElement.classList.add('mat-mdc-form-field-input-control');
@@ -223,4 +234,9 @@ export class MatChipInput implements MatChipTextControl, OnChanges, OnDestroy {
223234
private _isSeparatorKey(event: KeyboardEvent) {
224235
return !hasModifierKey(event) && new Set(this.separatorKeyCodes).has(event.keyCode);
225236
}
237+
238+
/** Gets the value to set on the `readonly` attribute. */
239+
protected _getReadonlyAttribute(): string | null {
240+
return this.readonly || (this.disabled && this.disabledInteractive) ? 'true' : null;
241+
}
226242
}

Diff for: ‎src/material/chips/testing/BUILD.bazel

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ ts_project(
99
["**/*.ts"],
1010
exclude = ["**/*.spec.ts"],
1111
),
12+
interop_deps = [
13+
"//src/cdk/coercion",
14+
],
1215
deps = [
1316
"//src/cdk/testing:testing_rjs",
1417
],

Diff for: ‎src/material/chips/testing/chip-input-harness.spec.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ describe('MatChipInputHarness', () => {
3838
expect(await harnesses[1].isDisabled()).toBe(true);
3939
});
4040

41+
it('should get the disabled state when disabledInteractive is enabled', async () => {
42+
fixture.componentInstance.disabledInteractive = true;
43+
fixture.changeDetectorRef.markForCheck();
44+
const harnesses = await loader.getAllHarnesses(MatChipInputHarness);
45+
expect(await harnesses[0].isDisabled()).toBe(false);
46+
expect(await harnesses[1].isDisabled()).toBe(true);
47+
});
48+
4149
it('should get whether the input is required', async () => {
4250
const harness = await loader.getHarness(MatChipInputHarness);
4351
expect(await harness.isRequired()).toBe(false);
@@ -91,7 +99,10 @@ describe('MatChipInputHarness', () => {
9199
</mat-chip-grid>
92100
93101
<mat-chip-grid #grid2>
94-
<input [matChipInputFor]="grid2" disabled />
102+
<input
103+
[matChipInputFor]="grid2"
104+
[matChipInputDisabledInteractive]="disabledInteractive"
105+
disabled/>
95106
</mat-chip-grid>
96107
`,
97108
imports: [MatChipsModule],
@@ -100,4 +111,5 @@ class ChipInputHarnessTest {
100111
required = false;
101112
add = jasmine.createSpy('add spy');
102113
separatorKeyCodes = [COMMA];
114+
disabledInteractive = false;
103115
}

Diff for: ‎src/material/chips/testing/chip-input-harness.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
HarnessPredicate,
1313
TestKey,
1414
} from '@angular/cdk/testing';
15+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
1516
import {ChipInputHarnessFilters} from './chip-harness-filters';
1617

1718
/** Harness for interacting with a grid's chip input in tests. */
@@ -42,7 +43,14 @@ export class MatChipInputHarness extends ComponentHarness {
4243

4344
/** Whether the input is disabled. */
4445
async isDisabled(): Promise<boolean> {
45-
return (await this.host()).getProperty<boolean>('disabled');
46+
const host = await this.host();
47+
const disabled = await host.getAttribute('disabled');
48+
49+
if (disabled !== null) {
50+
return coerceBooleanProperty(disabled);
51+
}
52+
53+
return (await host.getAttribute('aria-disabled')) === 'true';
4654
}
4755

4856
/** Whether the input is required. */

Diff for: ‎src/material/chips/tokens.ts

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export interface MatChipsDefaultOptions {
1616

1717
/** Whether icon indicators should be hidden for single-selection. */
1818
hideSingleSelectionIndicator?: boolean;
19+
20+
/** Whether the chip input should be interactive while disabled by default. */
21+
inputDisabledInteractive?: boolean;
1922
}
2023

2124
/** Injection token to be used to override the default options for the chips module. */

0 commit comments

Comments
 (0)
Please sign in to comment.