diff --git a/packages/mdc-list/foundation.ts b/packages/mdc-list/foundation.ts index 7505ae96aa6..926339a6678 100644 --- a/packages/mdc-list/foundation.ts +++ b/packages/mdc-list/foundation.ts @@ -413,7 +413,7 @@ export class MDCListFoundation extends MDCFoundation { this.toggleCheckboxRange( currentIndex, this.adapter.getListItemCount() - 1, currentIndex); } else if ( - isLetterA && eventHasModifiers(['Control']) && this.isCheckboxList) { + isLetterA && (eventHasModifiers(['Control']) || eventHasModifiers(['Meta'])) && this.isCheckboxList) { event.preventDefault(); this.checkboxListToggleAll( this.selectedIndex === numbers.UNSET_INDEX ? diff --git a/packages/mdc-list/test/foundation.test.ts b/packages/mdc-list/test/foundation.test.ts index 667ea0965bb..2045fed293a 100644 --- a/packages/mdc-list/test/foundation.test.ts +++ b/packages/mdc-list/test/foundation.test.ts @@ -1371,6 +1371,164 @@ describe('MDCListFoundation', () => { .toHaveBeenCalledWith(2, true); }); + it('#handleKeydown should select all items on command(metaKey) + A, if nothing is selected', + () => { + const {foundation, mockAdapter} = setupTest(); + const event = createMockKeyboardEvent('A', ['Meta']); + + mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true); + mockAdapter.getListItemCount.and.returnValue(3); + foundation.layout(); + foundation.handleKeydown(event, false, -1); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledTimes(3); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(0, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(1, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(2, true); + }); + + it('#handleKeydown should select all items on command(metaKey) + lowercase A, if nothing is selected', + () => { + const {foundation, mockAdapter} = setupTest(); + const event = createMockKeyboardEvent('a', ['Meta']); + + mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true); + mockAdapter.getListItemCount.and.returnValue(3); + foundation.layout(); + foundation.handleKeydown(event, false, -1); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledTimes(3); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(0, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(1, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(2, true); + }); + + it('#handleKeydown should select all items on command(metaKey) + A, if some items are selected', + () => { + const {foundation, mockAdapter} = setupTest(); + const event = createMockKeyboardEvent('A', ['Meta']); + + mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true); + mockAdapter.getListItemCount.and.returnValue(4); + foundation.layout(); + foundation.setSelectedIndex([1, 2]); + + // Reset the calls since `setSelectedIndex` will throw it off. + mockAdapter.setCheckedCheckboxOrRadioAtIndex.calls.reset(); + foundation.handleKeydown(event, false, -1); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledTimes(4); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(0, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(1, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(2, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(3, true); + }); + + it('#handleKeydown should deselect all items on command(metaKey) + A, if all items are selected', + () => { + const {foundation, mockAdapter} = setupTest(); + const event = createMockKeyboardEvent('A', ['Meta']); + + mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true); + mockAdapter.getListItemCount.and.returnValue(3); + foundation.layout(); + foundation.setSelectedIndex([0, 1, 2]); + + // Reset the calls since `setSelectedIndex` will throw it off. + mockAdapter.setCheckedCheckboxOrRadioAtIndex.calls.reset(); + foundation.handleKeydown(event, false, -1); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledTimes(3); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(0, false); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(1, false); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(2, false); + }); + + it('#handleKeydown should not select disabled items on command(metaKey) + A', () => { + const {foundation, mockAdapter} = setupTest(); + const event = createMockKeyboardEvent('A', ['Meta']); + + mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true); + mockAdapter.listItemAtIndexHasClass + .withArgs(1, cssClasses.LIST_ITEM_DISABLED_CLASS) + .and.returnValue(true); + mockAdapter.getListItemCount.and.returnValue(3); + foundation.layout(); + foundation.handleKeydown(event, false, -1); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledTimes(3); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(0, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(1, false); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(2, true); + }); + + it('#handleKeydown should not handle command(metaKey) + A on a non-checkbox list', () => { + const {foundation, mockAdapter} = setupTest(); + const event = createMockKeyboardEvent('a', ['Meta']); + + mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(false); + mockAdapter.getListItemCount.and.returnValue(3); + foundation.layout(); + foundation.handleKeydown(event, false, -1); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex).not.toHaveBeenCalled(); + }); + + it('#handleKeydown should not deselect a selected disabled item on command(metaKey) + A', + () => { + const {foundation, mockAdapter} = setupTest(); + const event = createMockKeyboardEvent('A', ['Meta']); + + mockAdapter.hasCheckboxAtIndex.withArgs(0).and.returnValue(true); + mockAdapter.listItemAtIndexHasClass + .withArgs(1, cssClasses.LIST_ITEM_DISABLED_CLASS) + .and.returnValue(true); + mockAdapter.getListItemCount.and.returnValue(3); + foundation.layout(); + foundation.setSelectedIndex([1]); + + // Reset the calls since `setSelectedIndex` will throw it off. + mockAdapter.setCheckedCheckboxOrRadioAtIndex.calls.reset(); + foundation.handleKeydown(event, false, -1); + + expect(event.preventDefault).toHaveBeenCalledTimes(1); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledTimes(3); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(0, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(1, true); + expect(mockAdapter.setCheckedCheckboxOrRadioAtIndex) + .toHaveBeenCalledWith(2, true); + }); + it('#focusNextElement retains the focus on last item when wrapFocus=false and returns that index', () => { const {foundation, mockAdapter} = setupTest();