Skip to content
This repository was archived by the owner on Jan 13, 2025. It is now read-only.

Commit b659d4f

Browse files
Merge pull request #6566 from crisbeto:6366/list-ctrl-a
PiperOrigin-RevId: 342745079
2 parents 23491cf + eefef49 commit b659d4f

File tree

2 files changed

+160
-0
lines changed

2 files changed

+160
-0
lines changed

packages/mdc-list/foundation.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
255255
const isEnter = normalizeKey(event) === 'Enter';
256256
const isSpace = normalizeKey(event) === 'Spacebar';
257257

258+
// Have to check both upper and lower case, because having caps lock on affects the value.
259+
const isLetterA = event.key === 'A' || event.key === 'a';
260+
258261
if (this.adapter.isRootFocused()) {
259262
if (isArrowUp || isEnd) {
260263
event.preventDefault();
@@ -308,6 +311,9 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
308311
} else if (isEnd) {
309312
preventDefaultEvent(event);
310313
this.focusLastElement();
314+
} else if (isLetterA && event.ctrlKey && this.isCheckboxList_) {
315+
event.preventDefault();
316+
this.toggleAll(this.selectedIndex_ === numbers.UNSET_INDEX ? [] : this.selectedIndex_ as number[]);
311317
} else if (isEnter || isSpace) {
312318
if (isRootListItem) {
313319
// Return early if enter key is pressed on anchor element which triggers
@@ -667,6 +673,25 @@ export class MDCListFoundation extends MDCFoundation<MDCListAdapter> {
667673
this.focusedItemIndex = index;
668674
}
669675

676+
private toggleAll(currentlySelectedIndexes: number[]) {
677+
const count = this.adapter.getListItemCount();
678+
679+
// If all items are selected, deselect everything.
680+
if (currentlySelectedIndexes.length === count) {
681+
this.setCheckboxAtIndex_([]);
682+
} else {
683+
// Otherwise select all enabled options.
684+
const allIndexes: number[] = [];
685+
for (let i = 0; i < count; i++) {
686+
if (!this.adapter.listItemAtIndexHasClass(i, cssClasses.LIST_ITEM_DISABLED_CLASS) ||
687+
currentlySelectedIndexes.indexOf(i) > -1) {
688+
allIndexes.push(i);
689+
}
690+
}
691+
this.setCheckboxAtIndex_(allIndexes);
692+
}
693+
}
694+
670695
/**
671696
* Given the next desired character from the user, adds it to the typeahead
672697
* buffer. Then, attempts to find the next option matching the buffer. Wraps

packages/mdc-list/test/foundation.test.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -895,6 +895,141 @@ describe('MDCListFoundation', () => {
895895
expect(mockAdapter.focusItemAtIndex).toHaveBeenCalledTimes(1);
896896
});
897897

898+
it('#handleKeydown should select all items on ctrl + A, if nothing is selected', () => {
899+
const {foundation, mockAdapter} = setupTest();
900+
const preventDefault = jasmine.createSpy('preventDefault');
901+
const event = {key: 'A', ctrlKey: true, preventDefault};
902+
903+
mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true);
904+
mockAdapter.getListItemCount.and.returnValue(3);
905+
foundation.layout();
906+
foundation.handleKeydown(event, false, -1);
907+
908+
expect(preventDefault).toHaveBeenCalledTimes(1);
909+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledTimes(3);
910+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(0, true);
911+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(1, true);
912+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(2, true);
913+
});
914+
915+
it('#handleKeydown should select all items on ctrl + lowercase A, if nothing is selected', () => {
916+
const {foundation, mockAdapter} = setupTest();
917+
const preventDefault = jasmine.createSpy('preventDefault');
918+
const event = {key: 'a', ctrlKey: true, preventDefault};
919+
920+
mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true);
921+
mockAdapter.getListItemCount.and.returnValue(3);
922+
foundation.layout();
923+
foundation.handleKeydown(event, false, -1);
924+
925+
expect(preventDefault).toHaveBeenCalledTimes(1);
926+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledTimes(3);
927+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(0, true);
928+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(1, true);
929+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(2, true);
930+
});
931+
932+
it('#handleKeydown should select all items on ctrl + A, if some items are selected', () => {
933+
const {foundation, mockAdapter} = setupTest();
934+
const preventDefault = jasmine.createSpy('preventDefault');
935+
const event = {key: 'A', ctrlKey: true, preventDefault};
936+
937+
mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true);
938+
mockAdapter.getListItemCount.and.returnValue(4);
939+
foundation.layout();
940+
foundation.setSelectedIndex([1, 2]);
941+
942+
// Reset the calls since `setSelectedIndex` will throw it off.
943+
mockAdapter.setCheckedCheckboxOrRadioAtIndex.calls.reset();
944+
foundation.handleKeydown(event, false, -1);
945+
946+
expect(preventDefault).toHaveBeenCalledTimes(1);
947+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledTimes(4);
948+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(0, true);
949+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(1, true);
950+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(2, true);
951+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(3, true);
952+
});
953+
954+
it('#handleKeydown should deselect all items on ctrl + A, if all items are selected', () => {
955+
const {foundation, mockAdapter} = setupTest();
956+
const preventDefault = jasmine.createSpy('preventDefault');
957+
const event = {key: 'A', ctrlKey: true, preventDefault};
958+
959+
mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true);
960+
mockAdapter.getListItemCount.and.returnValue(3);
961+
foundation.layout();
962+
foundation.setSelectedIndex([0, 1, 2]);
963+
964+
// Reset the calls since `setSelectedIndex` will throw it off.
965+
mockAdapter.setCheckedCheckboxOrRadioAtIndex.calls.reset();
966+
foundation.handleKeydown(event, false, -1);
967+
968+
expect(preventDefault).toHaveBeenCalledTimes(1);
969+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledTimes(3);
970+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(0, false);
971+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(1, false);
972+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(2, false);
973+
});
974+
975+
it('#handleKeydown should not select disabled items on ctrl + A', () => {
976+
const {foundation, mockAdapter} = setupTest();
977+
const preventDefault = jasmine.createSpy('preventDefault');
978+
const event = {key: 'A', ctrlKey: true, preventDefault};
979+
980+
mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true);
981+
mockAdapter.listItemAtIndexHasClass
982+
.withArgs(1, cssClasses.LIST_ITEM_DISABLED_CLASS)
983+
.and.returnValue(true);
984+
mockAdapter.getListItemCount.and.returnValue(3);
985+
foundation.layout();
986+
foundation.handleKeydown(event, false, -1);
987+
988+
expect(preventDefault).toHaveBeenCalledTimes(1);
989+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledTimes(3);
990+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(0, true);
991+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(1, false);
992+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(2, true);
993+
});
994+
995+
it('#handleKeydown should not handle ctrl + A on a non-checkbox list', () => {
996+
const {foundation, mockAdapter} = setupTest();
997+
const preventDefault = jasmine.createSpy('preventDefault');
998+
const event = {key: 'a', ctrlKey: true, preventDefault};
999+
1000+
mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(false);
1001+
mockAdapter.getListItemCount.and.returnValue(3);
1002+
foundation.layout();
1003+
foundation.handleKeydown(event, false, -1);
1004+
1005+
expect(preventDefault).not.toHaveBeenCalled();
1006+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).not.toHaveBeenCalled();
1007+
});
1008+
1009+
it('#handleKeydown should not deselect a selected disabled item on ctrl + A', () => {
1010+
const {foundation, mockAdapter} = setupTest();
1011+
const preventDefault = jasmine.createSpy('preventDefault');
1012+
const event = {key: 'A', ctrlKey: true, preventDefault};
1013+
1014+
mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true);
1015+
mockAdapter.listItemAtIndexHasClass
1016+
.withArgs(1, cssClasses.LIST_ITEM_DISABLED_CLASS)
1017+
.and.returnValue(true);
1018+
mockAdapter.getListItemCount.and.returnValue(3);
1019+
foundation.layout();
1020+
foundation.setSelectedIndex([1]);
1021+
1022+
// Reset the calls since `setSelectedIndex` will throw it off.
1023+
mockAdapter.setCheckedCheckboxOrRadioAtIndex.calls.reset();
1024+
foundation.handleKeydown(event, false, -1);
1025+
1026+
expect(preventDefault).toHaveBeenCalledTimes(1);
1027+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledTimes(3);
1028+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(0, true);
1029+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(1, true);
1030+
expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).toHaveBeenCalledWith(2, true);
1031+
});
1032+
8981033
it('#focusNextElement retains the focus on last item when wrapFocus=false and returns that index',
8991034
() => {
9001035
const {foundation, mockAdapter} = setupTest();

0 commit comments

Comments
 (0)