From abaf57c687ab1fa1f07612dce13808bb5e050d25 Mon Sep 17 00:00:00 2001 From: Zdog1206 <148791675+Zdog1206@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:10:44 -0500 Subject: [PATCH 1/5] Update useSelectableCollection.ts Fix issue #9648 such that pressing CMD + left/right arrow actually triggers the browser's forward and back actions instead of moving between the tabs on the screen. --- packages/@react-aria/selection/src/useSelectableCollection.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index 796d7dcaa60..c33468d3076 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -168,6 +168,10 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } }; + if ((e.metaKey || e.altKey) && e.key.startsWith('Arrow')) { + return; + } + switch (e.key) { case 'ArrowDown': { if (delegate.getKeyBelow) { From ceee89e642eec2f89a5e96a8bb573dccb9da26a2 Mon Sep 17 00:00:00 2001 From: Zdog1206 <148791675+Zdog1206@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:22:45 -0500 Subject: [PATCH 2/5] Update useSelectableCollection.ts Another user requested the code be written slightly differently to avoid issues in other parts of the program. --- packages/@react-aria/selection/src/useSelectableCollection.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index c33468d3076..a0de95bf296 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -168,7 +168,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } }; - if ((e.metaKey || e.altKey) && e.key.startsWith('Arrow')) { + if (manager.selectionMode === 'single' && (e.metaKey || e.altKey)) { return; } From f7536103fa4d8af1162d783b46e8cd76fdca4342 Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Wed, 18 Mar 2026 16:59:44 +1100 Subject: [PATCH 3/5] scope down the behaviour --- .../selection/src/useSelectableCollection.ts | 42 +++++++++++++++-- .../test/ListBox.test.js | 47 +++++++++++++++++++ .../react-aria-components/test/Tabs.test.js | 24 ++++++++++ 3 files changed, 109 insertions(+), 4 deletions(-) diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index e4440a4aa53..239fb911925 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -10,7 +10,7 @@ * governing permissions and limitations under the License. */ -import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, getActiveElement, getEventTarget, isCtrlKeyPressed, isFocusWithin, isTabbable, mergeProps, nodeContains, scrollIntoView, scrollIntoViewport, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils'; +import {CLEAR_FOCUS_EVENT, FOCUS_EVENT, focusWithoutScrolling, getActiveElement, getEventTarget, isAppleDevice, isCtrlKeyPressed, isFocusWithin, isTabbable, mergeProps, nodeContains, scrollIntoView, scrollIntoViewport, useEvent, useRouter, useUpdateLayoutEffect} from '@react-aria/utils'; import {dispatchVirtualFocus, getFocusableTreeWalker, moveVirtualFocus} from '@react-aria/focus'; import {DOMAttributes, FocusableElement, FocusStrategy, Key, KeyboardDelegate, RefObject} from '@react-types/shared'; import {flushSync} from 'react-dom'; @@ -137,6 +137,8 @@ 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) => { if (key != null) { if (manager.isLink(key) && linkBehavior === 'selection' && selectOnFocus && !isNonContiguousSelectionModifier(e)) { @@ -168,12 +170,14 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } }; - if (manager.selectionMode === 'single' && (e.metaKey || e.altKey)) { - return; - } + let shouldIgnoreModifierKeys = e.metaKey || (e.shiftKey && manager.selectionMode !== 'multiple') || (!isAppleDevice() ? e.altKey : e.ctrlKey); switch (e.key) { case 'ArrowDown': { + // inverse of navigateToKey's usage + if (shouldIgnoreModifierKeys) { + return; + } if (delegate.getKeyBelow) { let nextKey = manager.focusedKey != null ? delegate.getKeyBelow?.(manager.focusedKey) @@ -189,6 +193,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions break; } case 'ArrowUp': { + if (shouldIgnoreModifierKeys) { + return; + } if (delegate.getKeyAbove) { let nextKey = manager.focusedKey != null ? delegate.getKeyAbove?.(manager.focusedKey) @@ -204,6 +211,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions break; } case 'ArrowLeft': { + if (shouldIgnoreModifierKeys) { + return; + } if (delegate.getKeyLeftOf) { let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyLeftOf?.(manager.focusedKey) : delegate.getFirstKey?.(); if (nextKey == null && shouldFocusWrap) { @@ -217,6 +227,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions break; } case 'ArrowRight': { + if (shouldIgnoreModifierKeys) { + return; + } if (delegate.getKeyRightOf) { let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyRightOf?.(manager.focusedKey) : delegate.getFirstKey?.(); if (nextKey == null && shouldFocusWrap) { @@ -230,6 +243,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions 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; @@ -247,6 +263,9 @@ 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; @@ -264,6 +283,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } break; case 'PageDown': + if (shouldIgnoreModifierKeys) { + return; + } if (delegate.getKeyPageBelow && manager.focusedKey != null) { let nextKey = delegate.getKeyPageBelow(manager.focusedKey); if (nextKey != null) { @@ -273,6 +295,9 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } break; case 'PageUp': + if (shouldIgnoreModifierKeys) { + return; + } if (delegate.getKeyPageAbove && manager.focusedKey != null) { let nextKey = delegate.getKeyPageAbove(manager.focusedKey); if (nextKey != null) { @@ -282,12 +307,18 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } break; case 'a': + if (e.altKey || e.shiftKey || (isAppleDevice() ? e.ctrlKey : e.metaKey)) { + return; + } if (isCtrlKeyPressed(e) && manager.selectionMode === 'multiple' && disallowSelectAll !== true) { 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(); @@ -295,6 +326,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. diff --git a/packages/react-aria-components/test/ListBox.test.js b/packages/react-aria-components/test/ListBox.test.js index 5cad37237cb..040a82bd6b8 100644 --- a/packages/react-aria-components/test/ListBox.test.js +++ b/packages/react-aria-components/test/ListBox.test.js @@ -2007,3 +2007,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 bc18c2d1ee7..fbeb3f2c2c3 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -288,6 +288,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( From 4edc370d2458a3014c67cf0ed76d5c27165049fe Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 19 Mar 2026 07:22:22 +1100 Subject: [PATCH 4/5] simplify --- .../selection/src/useSelectableCollection.ts | 34 ++++--------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index 239fb911925..60598b37771 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -140,6 +140,11 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions // 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. @@ -150,6 +155,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); } @@ -167,17 +173,12 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } else if (selectOnFocus && !isNonContiguousSelectionModifier(e)) { manager.replaceSelection(key); } + e.preventDefault(); } }; - let shouldIgnoreModifierKeys = e.metaKey || (e.shiftKey && manager.selectionMode !== 'multiple') || (!isAppleDevice() ? e.altKey : e.ctrlKey); - switch (e.key) { case 'ArrowDown': { - // inverse of navigateToKey's usage - if (shouldIgnoreModifierKeys) { - return; - } if (delegate.getKeyBelow) { let nextKey = manager.focusedKey != null ? delegate.getKeyBelow?.(manager.focusedKey) @@ -186,16 +187,12 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions nextKey = delegate.getFirstKey?.(manager.focusedKey); } if (nextKey != null) { - e.preventDefault(); navigateToKey(nextKey); } } break; } case 'ArrowUp': { - if (shouldIgnoreModifierKeys) { - return; - } if (delegate.getKeyAbove) { let nextKey = manager.focusedKey != null ? delegate.getKeyAbove?.(manager.focusedKey) @@ -204,39 +201,30 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions nextKey = delegate.getLastKey?.(manager.focusedKey); } if (nextKey != null) { - e.preventDefault(); navigateToKey(nextKey); } } break; } case 'ArrowLeft': { - if (shouldIgnoreModifierKeys) { - return; - } if (delegate.getKeyLeftOf) { let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyLeftOf?.(manager.focusedKey) : delegate.getFirstKey?.(); if (nextKey == null && shouldFocusWrap) { nextKey = direction === 'rtl' ? delegate.getFirstKey?.(manager.focusedKey) : delegate.getLastKey?.(manager.focusedKey); } if (nextKey != null) { - e.preventDefault(); navigateToKey(nextKey, direction === 'rtl' ? 'first' : 'last'); } } break; } case 'ArrowRight': { - if (shouldIgnoreModifierKeys) { - return; - } if (delegate.getKeyRightOf) { let nextKey: Key | undefined | null = manager.focusedKey != null ? delegate.getKeyRightOf?.(manager.focusedKey) : delegate.getFirstKey?.(); if (nextKey == null && shouldFocusWrap) { nextKey = direction === 'rtl' ? delegate.getLastKey?.(manager.focusedKey) : delegate.getFirstKey?.(manager.focusedKey); } if (nextKey != null) { - e.preventDefault(); navigateToKey(nextKey, direction === 'rtl' ? 'last' : 'first'); } } @@ -283,25 +271,17 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions } break; case 'PageDown': - if (shouldIgnoreModifierKeys) { - return; - } if (delegate.getKeyPageBelow && manager.focusedKey != null) { let nextKey = delegate.getKeyPageBelow(manager.focusedKey); if (nextKey != null) { - e.preventDefault(); navigateToKey(nextKey); } } break; case 'PageUp': - if (shouldIgnoreModifierKeys) { - return; - } if (delegate.getKeyPageAbove && manager.focusedKey != null) { let nextKey = delegate.getKeyPageAbove(manager.focusedKey); if (nextKey != null) { - e.preventDefault(); navigateToKey(nextKey); } } From bbe8e6c7e98715586093f55d74299763b99df6ac Mon Sep 17 00:00:00 2001 From: Robert Snow Date: Thu, 19 Mar 2026 07:53:03 +1100 Subject: [PATCH 5/5] add stop propagation to appropriate key handlers --- packages/@react-aria/selection/src/useSelectableCollection.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/@react-aria/selection/src/useSelectableCollection.ts b/packages/@react-aria/selection/src/useSelectableCollection.ts index 60598b37771..85db6ee5cdc 100644 --- a/packages/@react-aria/selection/src/useSelectableCollection.ts +++ b/packages/@react-aria/selection/src/useSelectableCollection.ts @@ -238,6 +238,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions if (manager.focusedKey === null && e.shiftKey) { return; } + e.stopPropagation(); e.preventDefault(); let firstKey: Key | null = delegate.getFirstKey(manager.focusedKey, isCtrlKeyPressed(e)); manager.setFocusedKey(firstKey); @@ -258,6 +259,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions if (manager.focusedKey === null && e.shiftKey) { return; } + e.stopPropagation(); e.preventDefault(); let lastKey = delegate.getLastKey(manager.focusedKey, isCtrlKeyPressed(e)); manager.setFocusedKey(lastKey); @@ -291,6 +293,7 @@ export function useSelectableCollection(options: AriaSelectableCollectionOptions return; } if (isCtrlKeyPressed(e) && manager.selectionMode === 'multiple' && disallowSelectAll !== true) { + e.stopPropagation(); e.preventDefault(); manager.selectAll(); }