From e0efa412c3a5059ba28b1befe7a4fa3fee1854ba Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Tue, 23 Sep 2025 16:07:40 -0400 Subject: [PATCH 1/4] fix(SelectPanel): do not bubble up keyboard events Co-authored-by: Tyler Jones --- package-lock.json | 18 +++++++------- .../react/src/SelectPanel/SelectPanel.tsx | 24 ++++++++++++++++++- packages/react/src/hooks/useMnemonics.ts | 8 +++---- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1bf95e6e2bf..e9f1662e913 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,7 +75,7 @@ "react-dom": "^18.3.1" }, "devDependencies": { - "@primer/react": "38.0.0-rc.2", + "@primer/react": "38.0.0-rc.3", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.3", @@ -88,7 +88,7 @@ "name": "example-nextjs", "version": "0.0.0", "dependencies": { - "@primer/react": "38.0.0-rc.2", + "@primer/react": "38.0.0-rc.3", "next": "^15.2.3", "react": "18.3.1", "react-dom": "18.3.1", @@ -104,7 +104,7 @@ "version": "0.0.0", "dependencies": { "@primer/octicons-react": "^19.14.0", - "@primer/react": "38.0.0-rc.2", + "@primer/react": "38.0.0-rc.3", "clsx": "^2.1.1", "next": "^15.2.3", "react": "18.3.1", @@ -25576,13 +25576,13 @@ }, "packages/mcp": { "name": "@primer/mcp", - "version": "0.0.4-rc.0", + "version": "0.1.0-rc.1", "dependencies": { "@babel/runtime": "^7.28.4", "@modelcontextprotocol/sdk": "^1.12.0", "@primer/octicons": "^19.15.5", "@primer/primitives": "10.x || 11.x", - "@primer/react": "^38.0.0-rc.0", + "@primer/react": "^38.0.0-rc.3", "cheerio": "^1.0.0", "turndown": "^7.2.0", "zod": "^3.23.8" @@ -25839,7 +25839,7 @@ }, "packages/react": { "name": "@primer/react", - "version": "38.0.0-rc.2", + "version": "38.0.0-rc.3", "license": "MIT", "dependencies": { "@github/mini-throttle": "^2.1.1", @@ -26413,11 +26413,11 @@ }, "packages/styled-react": { "name": "@primer/styled-react", - "version": "1.0.0-rc.2", + "version": "1.0.0-rc.3", "devDependencies": { "@babel/preset-react": "^7.27.1", "@babel/preset-typescript": "^7.27.1", - "@primer/react": "^38.0.0-rc.2", + "@primer/react": "^38.0.0-rc.3", "@rollup/plugin-babel": "^6.0.4", "@types/react": "18.3.11", "@types/react-dom": "18.3.1", @@ -26433,7 +26433,7 @@ "typescript": "^5.9.2" }, "peerDependencies": { - "@primer/react": "38.0.0-rc.2", + "@primer/react": "38.0.0-rc.3", "@types/react": "18.x || 19.x", "@types/react-dom": "18.x || 19.x", "@types/react-is": "18.x || 19.x", diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index 4c7dae093d9..b9fccede0d2 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 @@ -741,6 +742,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 as any)) 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) +} From e97259df6349841ba265d38618528cd7375bf344 Mon Sep 17 00:00:00 2001 From: Marie Lucca <40550942+francinelucca@users.noreply.github.com> Date: Tue, 23 Sep 2025 16:09:15 -0400 Subject: [PATCH 2/4] Fix keyboard event bubbling in SelectPanel --- .changeset/itchy-readers-yell.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/itchy-readers-yell.md 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 From a3a3da1736128372706348ae8b73a077bd2fea17 Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Tue, 23 Sep 2025 16:19:07 -0400 Subject: [PATCH 3/4] lint fix --- packages/react/src/SelectPanel/SelectPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index b9fccede0d2..32d98665f90 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -756,7 +756,7 @@ function Panel({ if (hasModifier) return // skip if it's not a alphabet key - if (!isAlphabetKey(event as any)) return + if (!isAlphabetKey(event.nativeEvent as KeyboardEvent)) return // if this is a typeahead event, don't propagate outside of menu event.stopPropagation() From 1859c0417233e1a75e95c76cd76ea1ef9ebcf890 Mon Sep 17 00:00:00 2001 From: Marie Lucca Date: Tue, 30 Sep 2025 15:48:37 -0400 Subject: [PATCH 4/4] preventBubbling behind FF --- packages/react/src/SelectPanel/SelectPanel.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react/src/SelectPanel/SelectPanel.tsx b/packages/react/src/SelectPanel/SelectPanel.tsx index 32d98665f90..0bc994590c7 100644 --- a/packages/react/src/SelectPanel/SelectPanel.tsx +++ b/packages/react/src/SelectPanel/SelectPanel.tsx @@ -216,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. @@ -794,7 +795,7 @@ function Panel({ } : {}), } as React.CSSProperties, - onKeyDown: preventBubbling(overlayProps?.onKeyDown), + onKeyDown: usingRemoveActiveDescendant ? preventBubbling(overlayProps?.onKeyDown) : overlayProps?.onKeyDown, }} focusTrapSettings={focusTrapSettings} focusZoneSettings={focusZoneSettings}