Skip to content

Commit 68d7ea0

Browse files
authored
fix(material/chips): provide ability to edit for all screen readers with a click on already focused chip (#30983)
1 parent 4b85f9f commit 68d7ea0

File tree

3 files changed

+122
-0
lines changed

3 files changed

+122
-0
lines changed

goldens/material/chips/index.api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,8 @@ export class MatChipRow extends MatChip implements AfterViewInit {
423423
editable: boolean;
424424
readonly edited: EventEmitter<MatChipEditedEvent>;
425425
// (undocumented)
426+
_handleClick(event: MouseEvent): void;
427+
// (undocumented)
426428
_handleDoubleclick(event: MouseEvent): void;
427429
_handleFocus(): void;
428430
// (undocumented)
@@ -434,6 +436,8 @@ export class MatChipRow extends MatChip implements AfterViewInit {
434436
// (undocumented)
435437
_isRippleDisabled(): boolean;
436438
// (undocumented)
439+
ngAfterViewInit(): void;
440+
// (undocumented)
437441
static ɵcmp: i0.ɵɵComponentDeclaration<MatChipRow, "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", never, { "editable": { "alias": "editable"; "required": false; }; }, { "edited": "edited"; }, ["contentEditInput"], ["mat-chip-avatar, [matChipAvatar]", "[matChipEditInput]", "*", "mat-chip-trailing-icon,[matChipRemove],[matChipTrailingIcon]"], true, never>;
438442
// (undocumented)
439443
static ɵfac: i0.ɵɵFactoryDeclaration<MatChipRow, never>;

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

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
dispatchEvent,
55
dispatchFakeEvent,
66
dispatchKeyboardEvent,
7+
dispatchMouseEvent,
78
provideFakeDirectionality,
89
} from '@angular/cdk/testing/private';
910
import {Component, DebugElement, ElementRef, ViewChild} from '@angular/core';
@@ -234,6 +235,90 @@ describe('Row Chips', () => {
234235
fixture.detectChanges();
235236
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
236237
});
238+
239+
it('should not begin editing on single click', () => {
240+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
241+
dispatchMouseEvent(chipNativeElement, 'click');
242+
fixture.detectChanges();
243+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
244+
});
245+
246+
it('should begin editing on single click when focused', fakeAsync(() => {
247+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
248+
chipNativeElement.focus();
249+
250+
// Need to also simulate the mousedown as that sets the already focused flag.
251+
dispatchMouseEvent(chipNativeElement, 'mousedown');
252+
dispatchMouseEvent(chipNativeElement, 'click');
253+
fixture.detectChanges();
254+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeTruthy();
255+
}));
256+
257+
describe('when disabled', () => {
258+
beforeEach(() => {
259+
testComponent.disabled = true;
260+
fixture.changeDetectorRef.markForCheck();
261+
fixture.detectChanges();
262+
});
263+
264+
it('should not begin editing on double click', () => {
265+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
266+
dispatchFakeEvent(chipNativeElement, 'dblclick');
267+
fixture.detectChanges();
268+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
269+
});
270+
271+
it('should not begin editing on ENTER', () => {
272+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
273+
dispatchKeyboardEvent(chipNativeElement, 'keydown', ENTER);
274+
fixture.detectChanges();
275+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
276+
});
277+
278+
it('should not begin editing on single click when focused', fakeAsync(() => {
279+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
280+
chipNativeElement.focus();
281+
282+
// Need to also simulate the mousedown as that sets the already focused flag.
283+
dispatchMouseEvent(chipNativeElement, 'mousedown');
284+
dispatchMouseEvent(chipNativeElement, 'click');
285+
fixture.detectChanges();
286+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
287+
}));
288+
});
289+
290+
describe('when not editable', () => {
291+
beforeEach(() => {
292+
testComponent.editable = false;
293+
fixture.changeDetectorRef.markForCheck();
294+
fixture.detectChanges();
295+
});
296+
297+
it('should not begin editing on double click', () => {
298+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
299+
dispatchFakeEvent(chipNativeElement, 'dblclick');
300+
fixture.detectChanges();
301+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
302+
});
303+
304+
it('should not begin editing on ENTER', () => {
305+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
306+
dispatchKeyboardEvent(chipNativeElement, 'keydown', ENTER);
307+
fixture.detectChanges();
308+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
309+
});
310+
311+
it('should not begin editing on single click when focused', fakeAsync(() => {
312+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
313+
chipNativeElement.focus();
314+
315+
// Need to also simulate the mousedown as that sets the already focused flag.
316+
dispatchMouseEvent(chipNativeElement, 'mousedown');
317+
dispatchMouseEvent(chipNativeElement, 'click');
318+
fixture.detectChanges();
319+
expect(chipNativeElement.querySelector('.mat-chip-edit-input')).toBeFalsy();
320+
}));
321+
});
237322
});
238323

239324
describe('editing behavior', () => {

src/material/chips/chip-row.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface MatChipEditedEvent extends MatChipEvent {
6060
'[attr.aria-description]': 'null',
6161
'[attr.role]': 'role',
6262
'(focus)': '_handleFocus()',
63+
'(click)': '_handleClick($event)',
6364
'(dblclick)': '_handleDoubleclick($event)',
6465
},
6566
providers: [
@@ -92,6 +93,15 @@ export class MatChipRow extends MatChip implements AfterViewInit {
9293
/** The projected chip edit input. */
9394
@ContentChild(MatChipEditInput) contentEditInput?: MatChipEditInput;
9495

96+
/**
97+
* Set on a mousedown when the chip is already focused via mouse or keyboard.
98+
*
99+
* This allows us to ensure chip is already focused when deciding whether to enter the
100+
* edit mode on a subsequent click. Otherwise, the chip appears focused when handling the
101+
* first click event.
102+
*/
103+
private _alreadyFocused = false;
104+
95105
_isEditing = false;
96106

97107
constructor(...args: unknown[]);
@@ -104,6 +114,19 @@ export class MatChipRow extends MatChip implements AfterViewInit {
104114
if (this._isEditing && !this._editStartPending) {
105115
this._onEditFinish();
106116
}
117+
this._alreadyFocused = false;
118+
});
119+
}
120+
121+
override ngAfterViewInit() {
122+
super.ngAfterViewInit();
123+
124+
// Sets _alreadyFocused (ahead of click) when chip already has focus.
125+
this._ngZone.runOutsideAngular(() => {
126+
this._elementRef.nativeElement.addEventListener(
127+
'mousedown',
128+
() => (this._alreadyFocused = this._hasFocus()),
129+
);
107130
});
108131
}
109132

@@ -135,6 +158,16 @@ export class MatChipRow extends MatChip implements AfterViewInit {
135158
}
136159
}
137160

161+
_handleClick(event: MouseEvent) {
162+
if (!this.disabled && this.editable && !this._isEditing && this._alreadyFocused) {
163+
// Ensure click event not picked up unintentionally by other listeners, as
164+
// once editing starts, the source element is detached from DOM.
165+
event.preventDefault();
166+
event.stopPropagation();
167+
this._startEditing(event);
168+
}
169+
}
170+
138171
_handleDoubleclick(event: MouseEvent) {
139172
if (!this.disabled && this.editable) {
140173
this._startEditing(event);

0 commit comments

Comments
 (0)