diff --git a/.changeset/itchy-readers-yell.md b/.changeset/itchy-readers-yell.md new file mode 100644 index 00000000000..510cde71b43 --- /dev/null +++ b/.changeset/itchy-readers-yell.md @@ -0,0 +1,5 @@ +--- +"@primer/react": patch +--- + +fix(SelectPanel): do not bubble up keyboard events diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index 4c7dae093d9..0bc994590c7 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -1,5 +1,5 @@ import {SearchIcon, TriangleDownIcon, XIcon, type IconProps} from '@primer/octicons-react' -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' +import React, {useCallback, useEffect, useMemo, useRef, useState, type KeyboardEventHandler} from 'react' import type {AnchoredOverlayProps} from '../AnchoredOverlay' import {AnchoredOverlay} from '../AnchoredOverlay' import type {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' @@ -27,6 +27,7 @@ import {debounce} from '@github/mini-throttle' import {useResponsiveValue} from '../hooks/useResponsiveValue' import type {ButtonProps, LinkButtonProps} from '../Button/types' import {Banner} from '../Banner' +import {isAlphabetKey} from '../hooks/useMnemonics' // we add a delay so that it does not interrupt default screen reader announcement and queues after it const SHORT_DELAY_MS = 500 @@ -215,6 +216,7 @@ function Panel({ const usingFullScreenOnNarrow = disableFullscreenOnNarrow ? false : featureFlagFullScreenOnNarrow const shouldOrderSelectedFirst = useFeatureFlag('primer_react_select_panel_order_selected_at_top') && showSelectedOptionsFirst + const usingRemoveActiveDescendant = useFeatureFlag('primer_react_select_panel_remove_active_descendant') // Single select modals work differently, they have an intermediate state where the user has selected an item but // has not yet confirmed the selection. This is the only time the user can cancel the selection. @@ -741,6 +743,26 @@ function Panel({ 'anchored', ) + const preventBubbling = + (customOnKeyDown: KeyboardEventHandler | undefined) => + (event: React.KeyboardEvent) => { + // skip if a TextInput has focus + customOnKeyDown?.(event) + + const activeElement = document.activeElement as HTMLElement + if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') return + + // skip if used with modifier to preserve shortcuts like ⌘ + F + const hasModifier = event.ctrlKey || event.altKey || event.metaKey + if (hasModifier) return + + // skip if it's not a alphabet key + if (!isAlphabetKey(event.nativeEvent as KeyboardEvent)) return + + // if this is a typeahead event, don't propagate outside of menu + event.stopPropagation() + } + return ( <> { - return event.key.length === 1 && /[a-z\d]/i.test(event.key) - } - return {containerRef} } + +export const isAlphabetKey = (event: KeyboardEvent) => { + return event.key.length === 1 && /[a-z\d]/i.test(event.key) +}