Skip to content

Commit 29f0bb2

Browse files
authored
feat(material/chips): make ChipInput optional for MatChipGrid (angular#31693)
1 parent df0d753 commit 29f0bb2

File tree

5 files changed

+144
-22
lines changed

5 files changed

+144
-22
lines changed

goldens/material/chips/index.api.md

Lines changed: 1 addition & 3 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;
@@ -216,8 +216,6 @@ export class MatChipGrid extends MatChipSet implements AfterContentInit, AfterVi
216216
// (undocumented)
217217
ngAfterContentInit(): void;
218218
// (undocumented)
219-
ngAfterViewInit(): void;
220-
// (undocumented)
221219
ngControl: NgControl;
222220
// (undocumented)
223221
ngDoCheck(): void;

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: 30 additions & 18 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,10 +97,11 @@ 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';
102103
private _errorStateTracker: _ErrorStateTracker;
104+
private _uid = inject(_IdGenerator).getId('mat-chip-grid-');
103105

104106
/**
105107
* List of element ids to propagate to the chipInput's aria-describedby attribute.
@@ -137,7 +139,7 @@ export class MatChipGrid
137139
* @docs-private
138140
*/
139141
get id(): string {
140-
return this._chipInput.id;
142+
return this._chipInput ? this._chipInput.id : this._uid;
141143
}
142144

143145
/**
@@ -166,7 +168,7 @@ export class MatChipGrid
166168

167169
/** Whether any chips or the matChipInput inside of this chip-grid has focus. */
168170
override get focused(): boolean {
169-
return this._chipInput.focused || this._hasFocusedChip();
171+
return this._chipInput?.focused || this._hasFocusedChip();
170172
}
171173

172174
/**
@@ -285,14 +287,6 @@ export class MatChipGrid
285287
.subscribe(() => this.stateChanges.next());
286288
}
287289

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-
296290
ngDoCheck() {
297291
if (this.ngControl) {
298292
// We need to re-evaluate this on every change detection cycle, because there are some
@@ -311,6 +305,9 @@ export class MatChipGrid
311305
registerInput(inputElement: MatChipTextControl): void {
312306
this._chipInput = inputElement;
313307
this._chipInput.setDescribedByIds(this._ariaDescribedbyIds);
308+
309+
// If ids were already attached to host element, can now remove in favor of chipInput
310+
this._elementRef.nativeElement.removeAttribute('aria-describedby');
314311
}
315312

316313
/**
@@ -328,14 +325,18 @@ export class MatChipGrid
328325
* are no eligible chips.
329326
*/
330327
override focus(): void {
331-
if (this.disabled || this._chipInput.focused) {
328+
if (this.disabled || this._chipInput?.focused) {
332329
return;
333330
}
334331

335332
if (!this._chips.length || this._chips.first.disabled) {
333+
if (!this._chipInput) {
334+
return;
335+
}
336+
336337
// Delay until the next tick, because this can cause a "changed after checked"
337338
// error if the input does something on focus (e.g. opens an autocomplete).
338-
Promise.resolve().then(() => this._chipInput.focus());
339+
Promise.resolve().then(() => this._chipInput!.focus());
339340
} else {
340341
const activeItem = this._keyManager.activeItem;
341342

@@ -354,7 +355,11 @@ export class MatChipGrid
354355
* @docs-private
355356
*/
356357
get describedByIds(): string[] {
357-
return this._chipInput?.describedByIds || [];
358+
if (this._chipInput) {
359+
return this._chipInput.describedByIds || [];
360+
}
361+
const existing = this._elementRef.nativeElement.getAttribute('aria-describedby');
362+
return existing ? existing.split(' ') : [];
358363
}
359364

360365
/**
@@ -365,7 +370,14 @@ export class MatChipGrid
365370
// We must keep this up to date to handle the case where ids are set
366371
// before the chip input is registered.
367372
this._ariaDescribedbyIds = ids;
368-
this._chipInput?.setDescribedByIds(ids);
373+
374+
if (this._chipInput) {
375+
this._chipInput.setDescribedByIds(ids);
376+
} else if (ids.length) {
377+
this._elementRef.nativeElement.setAttribute('aria-describedby', ids.join(' '));
378+
} else {
379+
this._elementRef.nativeElement.removeAttribute('aria-describedby');
380+
}
369381
}
370382

371383
/**
@@ -429,7 +441,7 @@ export class MatChipGrid
429441
* it back to the first chip, creating a focus trap, if it user tries to tab away.
430442
*/
431443
protected override _allowFocusEscape() {
432-
if (!this._chipInput.focused) {
444+
if (!this._chipInput?.focused) {
433445
super._allowFocusEscape();
434446
}
435447
}
@@ -441,7 +453,7 @@ export class MatChipGrid
441453

442454
if (keyCode === TAB) {
443455
if (
444-
this._chipInput.focused &&
456+
this._chipInput?.focused &&
445457
hasModifierKey(event, 'shiftKey') &&
446458
this._chips.length &&
447459
!this._chips.last.disabled
@@ -459,7 +471,7 @@ export class MatChipGrid
459471
// disabled chip left in the list.
460472
super._allowFocusEscape();
461473
}
462-
} else if (!this._chipInput.focused) {
474+
} else if (!this._chipInput?.focused) {
463475
// The up and down arrows are supposed to navigate between the individual rows in the grid.
464476
// We do this by filtering the actions down to the ones that have the same `_isPrimary`
465477
// 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)