Skip to content

Commit 5a97c03

Browse files
committed
fix(material/chips): navigate between rows on up/down arrow (#29364)
Currently the up/down arrows behave in the same way as left/right, however that's incorrect based on [the reference implementation](https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#ex2_label) which shows them navigating between the rows. These changes update the logic in the chip grid so that it matches the expected behavior. Fixes #29359. (cherry picked from commit 0519174)
1 parent f61b05f commit 5a97c03

File tree

2 files changed

+87
-27
lines changed

2 files changed

+87
-27
lines changed

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

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import {Direction, Directionality} from '@angular/cdk/bidi';
33
import {
44
BACKSPACE,
55
DELETE,
6+
DOWN_ARROW,
67
END,
78
ENTER,
89
HOME,
910
LEFT_ARROW,
1011
RIGHT_ARROW,
1112
SPACE,
1213
TAB,
14+
UP_ARROW,
1315
} from '@angular/cdk/keycodes';
1416
import {
1517
createKeyboardEvent,
@@ -309,6 +311,48 @@ describe('MDC-based MatChipGrid', () => {
309311
.withContext('Expected focused item not to have changed.')
310312
.toBe(previousActiveElement);
311313
});
314+
315+
it('should focus primary action in next row when pressing DOWN ARROW on primary action', () => {
316+
chips.first.focus();
317+
expect(document.activeElement).toBe(primaryActions[0]);
318+
319+
dispatchKeyboardEvent(primaryActions[0], 'keydown', DOWN_ARROW);
320+
fixture.detectChanges();
321+
322+
expect(document.activeElement).toBe(primaryActions[1]);
323+
});
324+
325+
it('should focus primary action in previous row when pressing UP ARROW on primary action', () => {
326+
const lastIndex = primaryActions.length - 1;
327+
chips.last.focus();
328+
expect(document.activeElement).toBe(primaryActions[lastIndex]);
329+
330+
dispatchKeyboardEvent(primaryActions[lastIndex], 'keydown', UP_ARROW);
331+
fixture.detectChanges();
332+
333+
expect(document.activeElement).toBe(primaryActions[lastIndex - 1]);
334+
});
335+
336+
it('should focus(trailing action in next row when pressing DOWN ARROW on(trailing action', () => {
337+
trailingActions[0].focus();
338+
expect(document.activeElement).toBe(trailingActions[0]);
339+
340+
dispatchKeyboardEvent(trailingActions[0], 'keydown', DOWN_ARROW);
341+
fixture.detectChanges();
342+
343+
expect(document.activeElement).toBe(trailingActions[1]);
344+
});
345+
346+
it('should focus trailing action in previous row when pressing UP ARROW on trailing action', () => {
347+
const lastIndex = trailingActions.length - 1;
348+
trailingActions[lastIndex].focus();
349+
expect(document.activeElement).toBe(trailingActions[lastIndex]);
350+
351+
dispatchKeyboardEvent(trailingActions[lastIndex], 'keydown', UP_ARROW);
352+
fixture.detectChanges();
353+
354+
expect(document.activeElement).toBe(trailingActions[lastIndex - 1]);
355+
});
312356
});
313357

314358
describe('RTL', () => {
@@ -1034,11 +1078,8 @@ describe('MDC-based MatChipGrid', () => {
10341078
template: `
10351079
<mat-chip-grid [tabIndex]="tabIndex" [role]="role" #chipGrid>
10361080
@for (i of chips; track i) {
1037-
<mat-chip-row
1038-
[editable]="editable">
1039-
{{name}} {{i + 1}}
1040-
</mat-chip-row>
1041-
}
1081+
<mat-chip-row [editable]="editable">{{name}} {{i + 1}}</mat-chip-row>
1082+
}
10421083
</mat-chip-grid>
10431084
<input name="test" [matChipInputFor]="chipGrid"/>`,
10441085
})
@@ -1056,8 +1097,8 @@ class StandardChipGrid {
10561097
<mat-label>Add a chip</mat-label>
10571098
<mat-chip-grid #chipGrid>
10581099
@for (chip of chips; track chip) {
1059-
<mat-chip-row (removed)="remove(chip)">{{chip}}</mat-chip-row>
1060-
}
1100+
<mat-chip-row (removed)="remove(chip)">{{chip}}</mat-chip-row>
1101+
}
10611102
</mat-chip-grid>
10621103
<input name="test" [matChipInputFor]="chipGrid"/>
10631104
</mat-form-field>
@@ -1081,10 +1122,10 @@ class FormFieldChipGrid {
10811122
<mat-label>New food...</mat-label>
10821123
<mat-chip-grid #chipGrid placeholder="Food" [formControl]="control">
10831124
@for (food of foods; track food) {
1084-
<mat-chip-row [value]="food.value" (removed)="remove(food)">
1085-
{{ food.viewValue }}
1086-
</mat-chip-row>
1087-
}
1125+
<mat-chip-row [value]="food.value" (removed)="remove(food)">
1126+
{{ food.viewValue }}
1127+
</mat-chip-row>
1128+
}
10881129
</mat-chip-grid>
10891130
<input
10901131
[matChipInputFor]="chipGrid"
@@ -1143,10 +1184,8 @@ class InputChipGrid {
11431184
<mat-form-field>
11441185
<mat-chip-grid #chipGrid [formControl]="formControl">
11451186
@for (food of foods; track food) {
1146-
<mat-chip-row [value]="food.value">
1147-
{{food.viewValue}}
1148-
</mat-chip-row>
1149-
}
1187+
<mat-chip-row [value]="food.value">{{food.viewValue}}</mat-chip-row>
1188+
}
11501189
</mat-chip-grid>
11511190
<input name="test" [matChipInputFor]="chipGrid"/>
11521191
<mat-hint>Please select a chip, or type to add a new chip</mat-hint>
@@ -1179,8 +1218,8 @@ class ChipGridWithFormErrorMessages {
11791218
template: `
11801219
<mat-chip-grid #chipGrid>
11811220
@for (i of numbers; track i) {
1182-
<mat-chip-row (removed)="remove(i)">{{i}}</mat-chip-row>
1183-
}
1221+
<mat-chip-row (removed)="remove(i)">{{i}}</mat-chip-row>
1222+
}
11841223
<input name="test" [matChipInputFor]="chipGrid"/>
11851224
</mat-chip-grid>`,
11861225
animations: [
@@ -1208,11 +1247,11 @@ class StandardChipGridWithAnimations {
12081247
<mat-form-field>
12091248
<mat-chip-grid #chipGrid>
12101249
@for (i of chips; track i) {
1211-
<mat-chip-row [value]="i" (removed)="removeChip($event)">
1212-
Chip {{i + 1}}
1213-
<span matChipRemove>Remove</span>
1214-
</mat-chip-row>
1215-
}
1250+
<mat-chip-row [value]="i" (removed)="removeChip($event)">
1251+
Chip {{i + 1}}
1252+
<span matChipRemove>Remove</span>
1253+
</mat-chip-row>
1254+
}
12161255
</mat-chip-grid>
12171256
<input name="test" [matChipInputFor]="chipGrid"/>
12181257
</mat-form-field>

src/material/chips/chip-grid.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import {Directionality} from '@angular/cdk/bidi';
10-
import {hasModifierKey, TAB} from '@angular/cdk/keycodes';
10+
import {DOWN_ARROW, hasModifierKey, TAB, UP_ARROW} from '@angular/cdk/keycodes';
1111
import {
1212
AfterContentInit,
1313
AfterViewInit,
@@ -426,7 +426,10 @@ export class MatChipGrid
426426

427427
/** Handles custom keyboard events. */
428428
override _handleKeydown(event: KeyboardEvent) {
429-
if (event.keyCode === TAB) {
429+
const keyCode = event.keyCode;
430+
const activeItem = this._keyManager.activeItem;
431+
432+
if (keyCode === TAB) {
430433
if (
431434
this._chipInput.focused &&
432435
hasModifierKey(event, 'shiftKey') &&
@@ -435,8 +438,8 @@ export class MatChipGrid
435438
) {
436439
event.preventDefault();
437440

438-
if (this._keyManager.activeItem) {
439-
this._keyManager.setActiveItem(this._keyManager.activeItem);
441+
if (activeItem) {
442+
this._keyManager.setActiveItem(activeItem);
440443
} else {
441444
this._focusLastChip();
442445
}
@@ -447,7 +450,25 @@ export class MatChipGrid
447450
super._allowFocusEscape();
448451
}
449452
} else if (!this._chipInput.focused) {
450-
super._handleKeydown(event);
453+
// The up and down arrows are supposed to navigate between the individual rows in the grid.
454+
// We do this by filtering the actions down to the ones that have the same `_isPrimary`
455+
// flag as the active action and moving focus between them ourseles instead of delegating
456+
// to the key manager. For more information, see #29359 and:
457+
// https://www.w3.org/WAI/ARIA/apg/patterns/grid/examples/layout-grids/#ex2_label
458+
if ((keyCode === UP_ARROW || keyCode === DOWN_ARROW) && activeItem) {
459+
const eligibleActions = this._chipActions.filter(
460+
action => action._isPrimary === activeItem._isPrimary && !this._skipPredicate(action),
461+
);
462+
const currentIndex = eligibleActions.indexOf(activeItem);
463+
const delta = event.keyCode === UP_ARROW ? -1 : 1;
464+
465+
event.preventDefault();
466+
if (currentIndex > -1 && this._isValidIndex(currentIndex + delta)) {
467+
this._keyManager.setActiveItem(eligibleActions[currentIndex + delta]);
468+
}
469+
} else {
470+
super._handleKeydown(event);
471+
}
451472
}
452473

453474
this.stateChanges.next();

0 commit comments

Comments
 (0)