|
1 | 1 | import {SearchIcon, TriangleDownIcon, XIcon, type IconProps} from '@primer/octicons-react' |
2 | | -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react' |
| 2 | +import React, {useCallback, useEffect, useMemo, useRef, useState, type KeyboardEventHandler} from 'react' |
3 | 3 | import type {AnchoredOverlayProps} from '../AnchoredOverlay' |
4 | 4 | import {AnchoredOverlay} from '../AnchoredOverlay' |
5 | 5 | import type {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' |
@@ -27,6 +27,7 @@ import {debounce} from '@github/mini-throttle' |
27 | 27 | import {useResponsiveValue} from '../hooks/useResponsiveValue' |
28 | 28 | import type {ButtonProps, LinkButtonProps} from '../Button/types' |
29 | 29 | import {Banner} from '../Banner' |
| 30 | +import {isAlphabetKey} from '../hooks/useMnemonics' |
30 | 31 |
|
31 | 32 | // we add a delay so that it does not interrupt default screen reader announcement and queues after it |
32 | 33 | const SHORT_DELAY_MS = 500 |
@@ -215,6 +216,7 @@ function Panel({ |
215 | 216 | const usingFullScreenOnNarrow = disableFullscreenOnNarrow ? false : featureFlagFullScreenOnNarrow |
216 | 217 | const shouldOrderSelectedFirst = |
217 | 218 | useFeatureFlag('primer_react_select_panel_order_selected_at_top') && showSelectedOptionsFirst |
| 219 | + const usingRemoveActiveDescendant = useFeatureFlag('primer_react_select_panel_remove_active_descendant') |
218 | 220 |
|
219 | 221 | // Single select modals work differently, they have an intermediate state where the user has selected an item but |
220 | 222 | // has not yet confirmed the selection. This is the only time the user can cancel the selection. |
@@ -741,6 +743,26 @@ function Panel({ |
741 | 743 | 'anchored', |
742 | 744 | ) |
743 | 745 |
|
| 746 | + const preventBubbling = |
| 747 | + (customOnKeyDown: KeyboardEventHandler<HTMLDivElement> | undefined) => |
| 748 | + (event: React.KeyboardEvent<HTMLDivElement>) => { |
| 749 | + // skip if a TextInput has focus |
| 750 | + customOnKeyDown?.(event) |
| 751 | + |
| 752 | + const activeElement = document.activeElement as HTMLElement |
| 753 | + if (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') return |
| 754 | + |
| 755 | + // skip if used with modifier to preserve shortcuts like ⌘ + F |
| 756 | + const hasModifier = event.ctrlKey || event.altKey || event.metaKey |
| 757 | + if (hasModifier) return |
| 758 | + |
| 759 | + // skip if it's not a alphabet key |
| 760 | + if (!isAlphabetKey(event.nativeEvent as KeyboardEvent)) return |
| 761 | + |
| 762 | + // if this is a typeahead event, don't propagate outside of menu |
| 763 | + event.stopPropagation() |
| 764 | + } |
| 765 | + |
744 | 766 | return ( |
745 | 767 | <> |
746 | 768 | <AnchoredOverlay |
@@ -773,6 +795,7 @@ function Panel({ |
773 | 795 | } |
774 | 796 | : {}), |
775 | 797 | } as React.CSSProperties, |
| 798 | + onKeyDown: usingRemoveActiveDescendant ? preventBubbling(overlayProps?.onKeyDown) : overlayProps?.onKeyDown, |
776 | 799 | }} |
777 | 800 | focusTrapSettings={focusTrapSettings} |
778 | 801 | focusZoneSettings={focusZoneSettings} |
|
0 commit comments