Skip to content

Commit 70e2b31

Browse files
committed
feat(material/chips): make ChipInput optional for MatChipGrid
1 parent fa90911 commit 70e2b31

File tree

5 files changed

+155
-25
lines changed

5 files changed

+155
-25
lines changed

goldens/material/chips/index.api.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi
189189
_blur(): void;
190190
readonly change: EventEmitter<MatChipGridChange>;
191191
get chipBlurChanges(): Observable<MatChipEvent>;
192-
protected _chipInput: MatChipTextControl;
192+
protected _chipInput?: MatChipTextControl;
193193
// (undocumented)
194194
_chips: QueryList<MatChipRow>;
195195
readonly controlType: string;
@@ -209,15 +209,16 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi
209209
_focusLastChip(): void;
210210
_handleKeydown(event: KeyboardEvent): void;
211211
get id(): string;
212+
set id(value: string);
213+
// (undocumented)
214+
protected _id: string;
212215
// (undocumented)
213216
static ngAcceptInputType_disabled: unknown;
214217
// (undocumented)
215218
static ngAcceptInputType_required: unknown;
216219
// (undocumented)
217220
ngAfterContentInit(): void;
218221
// (undocumented)
219-
ngAfterViewInit(): void;
220-
// (undocumented)
221222
ngControl: NgControl;
222223
// (undocumented)
223224
ngDoCheck(): void;
@@ -241,6 +242,8 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi
241242
setDisabledState(isDisabled: boolean): void;
242243
get shouldLabelFloat(): boolean;
243244
readonly stateChanges: Subject<void>;
245+
// (undocumented)
246+
protected _uid: string;
244247
updateErrorState(): void;
245248
get value(): any;
246249
set value(value: any);
@@ -249,7 +252,7 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi
249252
readonly valueChange: EventEmitter<any>;
250253
writeValue(value: any): void;
251254
// (undocumented)
252-
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipGrid, "mat-chip-grid", never, { "disabled": { "alias": "disabled"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "required": { "alias": "required"; "required": false; }; "value": { "alias": "value"; "required": false; }; "errorStateMatcher": { "alias": "errorStateMatcher"; "required": false; }; }, { "change": "change"; "valueChange": "valueChange"; }, ["_chips"], ["*"], true, never>;
255+
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipGrid, "mat-chip-grid", never, { "disabled": { "alias": "disabled"; "required": false; }; "id": { "alias": "id"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "required": { "alias": "required"; "required": false; }; "value": { "alias": "value"; "required": false; }; "errorStateMatcher": { "alias": "errorStateMatcher"; "required": false; }; }, { "change": "change"; "valueChange": "valueChange"; }, ["_chips"], ["*"], true, never>;
253256
// (undocumented)
254257
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipGrid, never>;
255258
}

src/dev-app/chips/chips-demo.html

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,31 @@ <h4>Options</h4>
230230
<mat-checkbox name="addOnBlur" [(ngModel)]="addOnBlur">Add on Blur</mat-checkbox>
231231
</p>
232232

233+
<h4>Chip grid with no Input</h4>
234+
235+
<mat-form-field class="demo-has-chip-list">
236+
<mat-chip-grid #chipGrid3 [(ngModel)]="selectedPeople" required [disabled]="disableInputs">
237+
@for (person of people; track person) {
238+
<mat-chip-row
239+
[editable]="editable"
240+
(removed)="remove(person)"
241+
(edited)="edit(person, $event)">
242+
@if (showEditIcon) {
243+
<button matChipEdit aria-label="Edit contributor">
244+
<mat-icon>edit</mat-icon>
245+
</button>
246+
}
247+
@if (peopleWithAvatar && person.avatar) {
248+
<mat-chip-avatar>{{person.avatar}}</mat-chip-avatar>
249+
}
250+
{{person.name}}
251+
<button matChipRemove aria-label="Remove contributor">
252+
<mat-icon>close</mat-icon>
253+
</button>
254+
</mat-chip-row>
255+
}
256+
</mat-chip-grid>
257+
</mat-form-field>
233258
</mat-card-content>
234259
</mat-card>
235260

src/material/chips/chip-grid.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,74 @@ describe('MatChipGrid', () => {
588588
});
589589
});
590590

591+
describe('ChipGrid without input', () => {
592+
it('should not throw when used without a chip input', () => {
593+
expect(() => createComponent(ChipGridWithoutInput)).not.toThrow();
594+
});
595+
596+
it('should be able to focus the first chip', () => {
597+
const fixture = createComponent(ChipGridWithoutInput);
598+
chipGridInstance.focus();
599+
fixture.detectChanges();
600+
expect(document.activeElement).toBe(primaryActions[0]);
601+
});
602+
603+
it('should not do anything on focus if there are no chips', () => {
604+
const fixture = createComponent(ChipGridWithoutInput);
605+
(testComponent as unknown as ChipGridWithoutInput).chips = [];
606+
fixture.changeDetectorRef.markForCheck();
607+
fixture.detectChanges();
608+
609+
chipGridInstance.focus();
610+
fixture.detectChanges();
611+
612+
expect(chipGridNativeElement.contains(document.activeElement)).toBe(false);
613+
});
614+
615+
it('should have a default id on the component instance', () => {
616+
createComponent(ChipGridWithoutInput);
617+
expect(chipGridInstance.id).toMatch(/^mat-chip-grid-\w+$/);
618+
});
619+
620+
it('should have empty getters that work without an input', () => {
621+
const fixture = createComponent(ChipGridWithoutInput);
622+
expect(chipGridInstance.empty).toBe(false);
623+
624+
(testComponent as unknown as ChipGridWithoutInput).chips = [];
625+
fixture.changeDetectorRef.markForCheck();
626+
fixture.detectChanges();
627+
628+
expect(chipGridInstance.empty).toBe(true);
629+
});
630+
631+
it('should have a placeholder getter that works without an input', () => {
632+
const fixture = createComponent(ChipGridWithoutInput);
633+
(testComponent as unknown as ChipGridWithoutInput).placeholder = 'Hello';
634+
fixture.changeDetectorRef.markForCheck();
635+
fixture.detectChanges();
636+
expect(chipGridInstance.placeholder).toBe('Hello');
637+
});
638+
639+
it('should have a focused getter that works without an input', () => {
640+
const fixture = createComponent(ChipGridWithoutInput);
641+
expect(chipGridInstance.focused).toBe(false);
642+
643+
chipGridInstance.focus();
644+
fixture.detectChanges();
645+
646+
expect(chipGridInstance.focused).toBe(true);
647+
});
648+
649+
it('should set aria-describedby on the grid when there is no input', fakeAsync(() => {
650+
const fixture = createComponent(ChipGridWithoutInput);
651+
const hint = fixture.debugElement.query(By.css('mat-hint')).nativeElement;
652+
flush();
653+
fixture.detectChanges();
654+
655+
expect(chipGridNativeElement.getAttribute('aria-describedby')).toBe(hint.id);
656+
}));
657+
});
658+
591659
describe('with chip remove', () => {
592660
it('should properly focus next item if chip is removed through click', fakeAsync(() => {
593661
// TODO(crisbeto): this test fails without the NoopAnimationsModule for some reason.
@@ -1234,3 +1302,22 @@ class ChipGridWithRemove {
12341302
this.chips.splice(event.chip.value, 1);
12351303
}
12361304
}
1305+
1306+
@Component({
1307+
template: `
1308+
<mat-form-field>
1309+
<mat-label>Foods</mat-label>
1310+
<mat-chip-grid #chipGrid [placeholder]="placeholder">
1311+
@for (food of chips; track food) {
1312+
<mat-chip-row>{{ food }}</mat-chip-row>
1313+
}
1314+
</mat-chip-grid>
1315+
<mat-hint>Some hint</mat-hint>
1316+
</mat-form-field>
1317+
`,
1318+
imports: [MatChipGrid, MatChipRow, MatFormField, MatLabel, MatHint],
1319+
})
1320+
class ChipGridWithoutInput {
1321+
chips = ['Pizza', 'Pasta', 'Tacos'];
1322+
placeholder: string;
1323+
}

src/material/chips/chip-grid.ts

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import {_IdGenerator} from '@angular/cdk/a11y';
910
import {DOWN_ARROW, hasModifierKey, TAB, UP_ARROW} from '@angular/cdk/keycodes';
1011
import {
1112
AfterContentInit,
@@ -96,9 +97,10 @@ export class MatChipGrid
9697
readonly controlType: string = 'mat-chip-grid';
9798

9899
/** The chip input to add more chips */
99-
protected _chipInput: MatChipTextControl;
100+
protected _chipInput?: MatChipTextControl;
100101

101102
protected override _defaultRole = 'grid';
103+
protected _uid = inject(_IdGenerator).getId('mat-chip-grid-');
102104
private _errorStateTracker: _ErrorStateTracker;
103105

104106
/**
@@ -136,9 +138,14 @@ export class MatChipGrid
136138
* Implemented as part of MatFormFieldControl.
137139
* @docs-private
138140
*/
141+
@Input()
139142
get id(): string {
140-
return this._chipInput.id;
143+
return this._chipInput ? this._chipInput.id : this._id;
144+
}
145+
set id(value: string) {
146+
this._id = value || this._uid;
141147
}
148+
protected _id: string;
142149

143150
/**
144151
* Implemented as part of MatFormFieldControl.
@@ -166,7 +173,7 @@ export class MatChipGrid
166173

167174
/** Whether any chips or the matChipInput inside of this chip-grid has focus. */
168175
override get focused(): boolean {
169-
return this._chipInput.focused || this._hasFocusedChip();
176+
return this._chipInput?.focused || this._hasFocusedChip();
170177
}
171178

172179
/**
@@ -272,6 +279,9 @@ export class MatChipGrid
272279
parentForm,
273280
this.stateChanges,
274281
);
282+
283+
// Force setter to be called in case id was not specified.
284+
this.id = this.id;
275285
}
276286

277287
ngAfterContentInit() {
@@ -285,14 +295,6 @@ export class MatChipGrid
285295
.subscribe(() => this.stateChanges.next());
286296
}
287297

288-
override ngAfterViewInit() {
289-
super.ngAfterViewInit();
290-
291-
if (!this._chipInput && (typeof ngDevMode === 'undefined' || ngDevMode)) {
292-
throw Error('mat-chip-grid must be used in combination with matChipInputFor.');
293-
}
294-
}
295-
296298
ngDoCheck() {
297299
if (this.ngControl) {
298300
// We need to re-evaluate this on every change detection cycle, because there are some
@@ -328,14 +330,18 @@ export class MatChipGrid
328330
* are no eligible chips.
329331
*/
330332
override focus(): void {
331-
if (this.disabled || this._chipInput.focused) {
333+
if (this.disabled || this._chipInput?.focused) {
332334
return;
333335
}
334336

335337
if (!this._chips.length || this._chips.first.disabled) {
338+
if (!this._chipInput) {
339+
return;
340+
}
341+
336342
// Delay until the next tick, because this can cause a "changed after checked"
337343
// error if the input does something on focus (e.g. opens an autocomplete).
338-
Promise.resolve().then(() => this._chipInput.focus());
344+
Promise.resolve().then(() => this._chipInput!.focus());
339345
} else {
340346
const activeItem = this._keyManager.activeItem;
341347

@@ -354,18 +360,27 @@ export class MatChipGrid
354360
* @docs-private
355361
*/
356362
get describedByIds(): string[] {
357-
return this._chipInput?.describedByIds || [];
363+
if (this._chipInput) {
364+
return this._chipInput.describedByIds || [];
365+
}
366+
const existing = this._elementRef.nativeElement.getAttribute('aria-describedby');
367+
return existing ? existing.split(' ') : [];
358368
}
359369

360370
/**
361371
* Implemented as part of MatFormFieldControl.
362372
* @docs-private
363373
*/
364374
setDescribedByIds(ids: string[]) {
365-
// We must keep this up to date to handle the case where ids are set
366-
// before the chip input is registered.
367375
this._ariaDescribedbyIds = ids;
368-
this._chipInput?.setDescribedByIds(ids);
376+
377+
if (this._chipInput) {
378+
this._chipInput.setDescribedByIds(ids);
379+
} else if (ids.length) {
380+
this._elementRef.nativeElement.setAttribute('aria-describedby', ids.join(' '));
381+
} else {
382+
this._elementRef.nativeElement.removeAttribute('aria-describedby');
383+
}
369384
}
370385

371386
/**
@@ -429,7 +444,7 @@ export class MatChipGrid
429444
* it back to the first chip, creating a focus trap, if it user tries to tab away.
430445
*/
431446
protected override _allowFocusEscape() {
432-
if (!this._chipInput.focused) {
447+
if (!this._chipInput?.focused) {
433448
super._allowFocusEscape();
434449
}
435450
}
@@ -441,7 +456,7 @@ export class MatChipGrid
441456

442457
if (keyCode === TAB) {
443458
if (
444-
this._chipInput.focused &&
459+
this._chipInput?.focused &&
445460
hasModifierKey(event, 'shiftKey') &&
446461
this._chips.length &&
447462
!this._chips.last.disabled
@@ -459,7 +474,7 @@ export class MatChipGrid
459474
// disabled chip left in the list.
460475
super._allowFocusEscape();
461476
}
462-
} else if (!this._chipInput.focused) {
477+
} else if (!this._chipInput?.focused) {
463478
// The up and down arrows are supposed to navigate between the individual rows in the grid.
464479
// We do this by filtering the actions down to the ones that have the same `_isPrimary`
465480
// flag as the active action and moving focus between them ourseles instead of delegating

src/material/chips/chips.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ Users can move through the chips using the arrow keys and select/deselect them w
3636

3737
Use `<mat-chip-grid>` and `<mat-chip-row>` for assisting users with text entry.
3838

39-
Chips are always used inside a container. To create chips connected to an input field, start by creating a `<mat-chip-grid>` as the container. Add an `<input/>` element, and register it to the `<mat-chip-grid>` by passing the `matChipInputFor` Input. Always use an `<input/>` element with `<mat-chip-grid>`. Nest a `<mat-chip-row>` element inside the `<mat-chip-grid>` for each piece of data entered by the user. An example of using chips for text input.
39+
Chips are always used inside a container. To create chips connected to an input field, start by creating a `<mat-chip-grid>` as the container. Add an `<input/>` element, and register it to the `<mat-chip-grid>` by passing the `matChipInputFor` Input. Nest a `<mat-chip-row>` element inside the `<mat-chip-grid>` for each piece of data entered by the user. An example of using chips for text input.
4040

4141
<!-- example(chips-input) -->
4242

0 commit comments

Comments
 (0)