Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/itchy-readers-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": patch
---

fix(SelectPanel): do not bubble up keyboard events
25 changes: 24 additions & 1 deletion packages/react/src/SelectPanel/SelectPanel.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -741,6 +743,26 @@ function Panel({
'anchored',
)

const preventBubbling =
(customOnKeyDown: KeyboardEventHandler<HTMLDivElement> | undefined) =>
(event: React.KeyboardEvent<HTMLDivElement>) => {
// skip if a TextInput has focus
customOnKeyDown?.(event)

const activeElement = document.activeElement as HTMLElement
if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️


// 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 (
<>
<AnchoredOverlay
Expand Down Expand Up @@ -773,6 +795,7 @@ function Panel({
}
: {}),
} as React.CSSProperties,
onKeyDown: usingRemoveActiveDescendant ? preventBubbling(overlayProps?.onKeyDown) : overlayProps?.onKeyDown,
}}
focusTrapSettings={focusTrapSettings}
focusZoneSettings={focusZoneSettings}
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/hooks/useMnemonics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,9 @@ export const useMnemonics = (open: boolean, providedRef?: React.RefObject<HTMLEl
[open, containerRef],
)

const isAlphabetKey = (event: KeyboardEvent) => {
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)
}
Loading