From 28c0d6e9b153e72d6344ecaf3c9617d5a763c9b9 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 2 Jan 2026 13:10:04 -0500 Subject: [PATCH 1/3] Ensure uncontrolled `ActionMenu` is handled properly when fullscreen --- .changeset/fast-pots-say.md | 5 +++++ packages/react/src/ActionMenu/ActionMenu.tsx | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/fast-pots-say.md diff --git a/.changeset/fast-pots-say.md b/.changeset/fast-pots-say.md new file mode 100644 index 00000000000..3b4a1f51029 --- /dev/null +++ b/.changeset/fast-pots-say.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +ActionMenu: Ensures that uncontrolled ActionMenu(s) retain tab-focus when fullscreen diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 7ed22316759..5800436e546 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -87,7 +87,11 @@ const Menu: FCWithSlotMarker> = ({ const isNarrow = useResponsiveValue({narrow: true}, false) const onClose: MenuCloseHandler = React.useCallback( gesture => { - if (isNarrow && open && gesture === 'tab') { + // If `open` is controlled, use that value to determine if we should proceed with close logic. + // If uncontrolled, use internal state (combinedOpenState) + const openStatus = open === undefined ? combinedOpenState : open + + if (isNarrow && openStatus && gesture === 'tab') { return } setCombinedOpenState(false) @@ -98,7 +102,7 @@ const Menu: FCWithSlotMarker> = ({ parentMenuContext.onClose?.(gesture) } }, - [setCombinedOpenState, parentMenuContext, open, isNarrow], + [setCombinedOpenState, parentMenuContext, open, isNarrow, combinedOpenState], ) const menuButtonChild = React.Children.toArray(children).find( From 9e15f74a3fefb50142f0f9d99cc000edd2391949 Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 2 Jan 2026 13:21:21 -0500 Subject: [PATCH 2/3] Address feedback --- packages/react/src/ActionMenu/ActionMenu.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 5800436e546..420af0f0217 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -87,11 +87,7 @@ const Menu: FCWithSlotMarker> = ({ const isNarrow = useResponsiveValue({narrow: true}, false) const onClose: MenuCloseHandler = React.useCallback( gesture => { - // If `open` is controlled, use that value to determine if we should proceed with close logic. - // If uncontrolled, use internal state (combinedOpenState) - const openStatus = open === undefined ? combinedOpenState : open - - if (isNarrow && openStatus && gesture === 'tab') { + if (isNarrow && combinedOpenState && gesture === 'tab') { return } setCombinedOpenState(false) @@ -102,7 +98,7 @@ const Menu: FCWithSlotMarker> = ({ parentMenuContext.onClose?.(gesture) } }, - [setCombinedOpenState, parentMenuContext, open, isNarrow, combinedOpenState], + [setCombinedOpenState, parentMenuContext, isNarrow, combinedOpenState], ) const menuButtonChild = React.Children.toArray(children).find( From dc7bf442b1d0b5f59fd413f53ad2128ab783806a Mon Sep 17 00:00:00 2001 From: Tyler Jones Date: Fri, 2 Jan 2026 14:14:55 -0500 Subject: [PATCH 3/3] Handle cases where fullscreen is not true, but narrow is --- packages/react/src/ActionMenu/ActionMenu.tsx | 23 +++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/react/src/ActionMenu/ActionMenu.tsx b/packages/react/src/ActionMenu/ActionMenu.tsx index 420af0f0217..7e97ae4d673 100644 --- a/packages/react/src/ActionMenu/ActionMenu.tsx +++ b/packages/react/src/ActionMenu/ActionMenu.tsx @@ -87,7 +87,7 @@ const Menu: FCWithSlotMarker> = ({ const isNarrow = useResponsiveValue({narrow: true}, false) const onClose: MenuCloseHandler = React.useCallback( gesture => { - if (isNarrow && combinedOpenState && gesture === 'tab') { + if (isNarrow && open && gesture === 'tab') { return } setCombinedOpenState(false) @@ -98,7 +98,7 @@ const Menu: FCWithSlotMarker> = ({ parentMenuContext.onClose?.(gesture) } }, - [setCombinedOpenState, parentMenuContext, isNarrow, combinedOpenState], + [setCombinedOpenState, parentMenuContext, open, isNarrow], ) const menuButtonChild = React.Children.toArray(children).find( @@ -285,12 +285,25 @@ const Overlay: FCWithSlotMarker> = ({ } = React.useContext(MenuContext) as MandateProps const containerRef = React.useRef(null) - useMenuKeyboardNavigation(open, onClose, containerRef, anchorRef, isSubmenu) const isNarrow = useResponsiveValue({narrow: true}, false) - const responsiveVariant = useResponsiveValue(variant, {regular: 'anchored', narrow: 'anchored'}) const isNarrowFullscreen = !!isNarrow && variant.narrow === 'fullscreen' + const handleClose: MenuCloseHandler = React.useCallback( + gesture => { + // In narrow fullscreen mode, don't close on tab, let focus stay in the menu + if (isNarrowFullscreen && gesture === 'tab') { + return + } + onClose?.(gesture) + }, + [isNarrowFullscreen, onClose], + ) + + useMenuKeyboardNavigation(open, handleClose, containerRef, anchorRef, isSubmenu) + + const responsiveVariant = useResponsiveValue(variant, {regular: 'anchored', narrow: 'anchored'}) + // If the menu anchor is an icon button, we need to label the menu by tooltip that also labelled the anchor. const [anchorAriaLabelledby, setAnchorAriaLabelledby] = useState(null) useEffect(() => { @@ -311,7 +324,7 @@ const Overlay: FCWithSlotMarker> = ({ anchorId={anchorId} open={open} onOpen={onOpen} - onClose={onClose} + onClose={handleClose} align={align} side={side ?? (isSubmenu ? 'outside-right' : 'outside-bottom')} overlayProps={overlayProps}