diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js
index 8f3326f95e8..e66cddda0bf 100644
--- a/packages/react-aria-components/test/ListBox.test.js
+++ b/packages/react-aria-components/test/ListBox.test.js
@@ -2001,3 +2001,50 @@ describe('ListBox', () => {
});
}
});
+
+describe('keyboard modifier keys', () => {
+ let user;
+ let platformMock;
+ beforeAll(() => {
+ user = userEvent.setup({delay: null, pointerMap});
+ });
+ // selectionMode: 'none', 'single', 'multiple'
+ // selectionBehavior: 'toggle', 'replace'
+ // platform: 'mac', 'windows'
+
+ // modifier key: 'alt', 'ctrl', 'meta', 'shift'
+ // key: 'arrow-up', 'arrow-down', 'arrow-left', 'arrow-right', 'home', 'end', 'page-up', 'page-down', 'enter', 'space', 'tab'
+ // expected behavior: 'navigate', 'select', 'toggle', 'replace'
+ describe('mac', () => {
+ beforeAll(() => {
+ platformMock = jest.spyOn(navigator, 'platform', 'get').mockImplementation(() => 'Mac');
+ });
+ afterAll(() => {
+ platformMock.mockRestore();
+ });
+ it('should not navigate when using unsupported modifier keys', async () => {
+ let {getByRole} = renderListbox({selectionMode: 'none'});
+ await user.tab();
+ let listbox = getByRole('listbox');
+ let options = within(listbox).getAllByRole('option');
+ await user.keyboard('{ArrowDown}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Meta>}{ArrowRight}{/Meta}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Meta>}{ArrowLeft}{/Meta}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Meta>}{ArrowDown}{/Meta}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Meta>}{ArrowUp}{/Meta}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Control>}{Home}{/Control}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Control>}{End}{/Control}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Meta>}{PageUp}{/Meta}');
+ expect(document.activeElement).toBe(options[1]);
+ await user.keyboard('{Meta>}{PageDown}{/Meta}');
+ expect(document.activeElement).toBe(options[1]);
+ });
+ });
+});
diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js
index 20f255979ca..fa106d5db73 100644
--- a/packages/react-aria-components/test/Tabs.test.js
+++ b/packages/react-aria-components/test/Tabs.test.js
@@ -291,6 +291,30 @@ describe('Tabs', () => {
expect(document.activeElement).toBe(items[2]);
});
+ it('should not navigate when using unsupported modifier keys', async () => {
+ let platformMock = jest.spyOn(navigator, 'platform', 'get').mockImplementation(() => 'Mac');
+ let {getAllByRole} = render(
+
+
+ A
+ B
+ C
+
+ A
+ B
+ C
+
+ );
+ let items = getAllByRole('tab');
+ expect(items[1]).toHaveAttribute('aria-disabled', 'true');
+
+ await user.tab();
+ expect(document.activeElement).toBe(items[0]);
+ await user.keyboard('{Meta>}{ArrowRight}{/Meta}');
+ expect(document.activeElement).toBe(items[0]);
+ platformMock.mockRestore();
+ });
+
it('finds the first non-disabled tab', async () => {
let {getAllByRole} = render(
diff --git a/packages/react-aria/src/selection/useSelectableCollection.ts b/packages/react-aria/src/selection/useSelectableCollection.ts
index 1628873baac..99d6014dfda 100644
--- a/packages/react-aria/src/selection/useSelectableCollection.ts
+++ b/packages/react-aria/src/selection/useSelectableCollection.ts
@@ -22,6 +22,7 @@ import {getActiveElement, getEventTarget, isFocusWithin, nodeContains} from '../
import {getFocusableTreeWalker} from '../focus/FocusScope';
import {getInteractionModality} from '../interactions/useFocusVisible';
import {getItemElement, isNonContiguousSelectionModifier, useCollectionId} from './utils';
+import {isAppleDevice} from '../utils/platform';
import {isCtrlKeyPressed} from '../utils/keyboard';
import {isTabbable} from '../utils/isFocusable';
import {mergeProps} from '../utils/mergeProps';
@@ -149,7 +150,14 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
return;
}
+ // uses shiftKey if selection mode is multiple
+ // if it's an apple device uses ctrlKey otherwise altKey
const navigateToKey = (key: Key | undefined, childFocus?: FocusStrategy) => {
+ let shouldIgnoreModifierKeys = e.metaKey || (e.shiftKey && manager.selectionMode !== 'multiple') || (!isAppleDevice() ? e.altKey : e.ctrlKey);
+ if (shouldIgnoreModifierKeys) {
+ return;
+ }
+
if (key != null) {
if (manager.isLink(key) && linkBehavior === 'selection' && selectOnFocus && !isNonContiguousSelectionModifier(e)) {
// Set focused key and re-render synchronously to bring item into view if needed.
@@ -160,6 +168,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
let item = getItemElement(ref, key);
let itemProps = manager.getItemProps(key);
if (item) {
+ e.preventDefault();
router.open(item, e, itemProps.href, itemProps.routerOptions);
}
@@ -177,6 +186,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
} else if (selectOnFocus && !isNonContiguousSelectionModifier(e)) {
manager.replaceSelection(key);
}
+ e.preventDefault();
}
};
@@ -190,7 +200,6 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
nextKey = delegate.getFirstKey?.(manager.focusedKey);
}
if (nextKey != null) {
- e.preventDefault();
navigateToKey(nextKey);
}
}
@@ -205,7 +214,6 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
nextKey = delegate.getLastKey?.(manager.focusedKey);
}
if (nextKey != null) {
- e.preventDefault();
navigateToKey(nextKey);
}
}
@@ -218,7 +226,6 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
nextKey = direction === 'rtl' ? delegate.getFirstKey?.(manager.focusedKey) : delegate.getLastKey?.(manager.focusedKey);
}
if (nextKey != null) {
- e.preventDefault();
navigateToKey(nextKey, direction === 'rtl' ? 'first' : 'last');
}
}
@@ -231,17 +238,20 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
nextKey = direction === 'rtl' ? delegate.getLastKey?.(manager.focusedKey) : delegate.getFirstKey?.(manager.focusedKey);
}
if (nextKey != null) {
- e.preventDefault();
navigateToKey(nextKey, direction === 'rtl' ? 'last' : 'first');
}
}
break;
}
case 'Home':
+ if (e.altKey || (e.shiftKey && manager.selectionMode !== 'multiple') || (isAppleDevice() ? e.ctrlKey : e.metaKey)) {
+ return;
+ }
if (delegate.getFirstKey) {
if (manager.focusedKey === null && e.shiftKey) {
return;
}
+ e.stopPropagation();
e.preventDefault();
let firstKey: Key | null = delegate.getFirstKey(manager.focusedKey, isCtrlKeyPressed(e));
manager.setFocusedKey(firstKey);
@@ -255,10 +265,14 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
}
break;
case 'End':
+ if (e.altKey || (e.shiftKey && manager.selectionMode !== 'multiple') || (isAppleDevice() ? e.ctrlKey : e.metaKey)) {
+ return;
+ }
if (delegate.getLastKey) {
if (manager.focusedKey === null && e.shiftKey) {
return;
}
+ e.stopPropagation();
e.preventDefault();
let lastKey = delegate.getLastKey(manager.focusedKey, isCtrlKeyPressed(e));
manager.setFocusedKey(lastKey);
@@ -275,7 +289,6 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
if (delegate.getKeyPageBelow && manager.focusedKey != null) {
let nextKey = delegate.getKeyPageBelow(manager.focusedKey);
if (nextKey != null) {
- e.preventDefault();
navigateToKey(nextKey);
}
}
@@ -284,18 +297,24 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
if (delegate.getKeyPageAbove && manager.focusedKey != null) {
let nextKey = delegate.getKeyPageAbove(manager.focusedKey);
if (nextKey != null) {
- e.preventDefault();
navigateToKey(nextKey);
}
}
break;
case 'a':
+ if (e.altKey || e.shiftKey || (isAppleDevice() ? e.ctrlKey : e.metaKey)) {
+ return;
+ }
if (isCtrlKeyPressed(e) && manager.selectionMode === 'multiple' && disallowSelectAll !== true) {
+ e.stopPropagation();
e.preventDefault();
manager.selectAll();
}
break;
case 'Escape':
+ if (e.altKey || e.shiftKey || e.metaKey || e.ctrlKey) {
+ return;
+ }
if (escapeKeyBehavior === 'clearSelection' && !disallowEmptySelection && manager.selectedKeys.size !== 0) {
e.stopPropagation();
e.preventDefault();
@@ -303,6 +322,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions
}
break;
case 'Tab': {
+ if (e.altKey || e.metaKey || e.ctrlKey) {
+ return;
+ }
if (!allowsTabNavigation) {
// There may be elements that are "tabbable" inside a collection (e.g. in a grid cell).
// However, collections should be treated as a single tab stop, with arrow key navigation internally.