From 07df108cee6e2baca20aa0639e8d529fa9695922 Mon Sep 17 00:00:00 2001 From: Josh Black Date: Tue, 12 May 2026 09:18:08 -0500 Subject: [PATCH 1/4] Replace deprecated ref helper usage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/clean-ref-helper-usage.md | 5 +++++ packages/react/src/ActionBar/ActionBar.tsx | 6 +++--- packages/react/src/ActionList/Heading.tsx | 6 +++--- packages/react/src/Autocomplete/AutocompleteInput.tsx | 6 +++--- .../react/src/Autocomplete/AutocompleteOverlay.tsx | 6 +++--- packages/react/src/Button/ButtonBase.tsx | 6 +++--- packages/react/src/Dialog/Dialog.tsx | 7 +++---- packages/react/src/Heading/Heading.tsx | 6 +++--- packages/react/src/Link/Link.tsx | 6 +++--- packages/react/src/Overlay/Overlay.tsx | 7 +++---- packages/react/src/PageLayout/PageLayout.tsx | 10 +++++----- .../src/TextInputWithTokens/TextInputWithTokens.tsx | 6 +++--- packages/react/src/deprecated/DialogV1/Dialog.tsx | 6 +++--- 13 files changed, 43 insertions(+), 40 deletions(-) create mode 100644 .changeset/clean-ref-helper-usage.md diff --git a/.changeset/clean-ref-helper-usage.md b/.changeset/clean-ref-helper-usage.md new file mode 100644 index 00000000000..0552e9e1bd7 --- /dev/null +++ b/.changeset/clean-ref-helper-usage.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Ref management: Replace internal `useRefObjectAsForwardedRef` usage with `useMergedRefs` diff --git a/packages/react/src/ActionBar/ActionBar.tsx b/packages/react/src/ActionBar/ActionBar.tsx index a2c03125535..fa9acdb0a4c 100644 --- a/packages/react/src/ActionBar/ActionBar.tsx +++ b/packages/react/src/ActionBar/ActionBar.tsx @@ -12,7 +12,7 @@ import {ActionMenu} from '../ActionMenu' import {useFocusZone, FocusKeys} from '../hooks/useFocusZone' import styles from './ActionBar.module.css' import {clsx} from 'clsx' -import {useRefObjectAsForwardedRef} from '../hooks' +import {useMergedRefs} from '../hooks' import {createDescendantRegistry} from '../utils/descendant-registry' const ACTIONBAR_ITEM_GAP = 8 @@ -470,7 +470,7 @@ function useWidth(ref: React.RefObject) { export const ActionBarIconButton = forwardRef( ({disabled, onClick, ...props}: ActionBarIconButtonProps, forwardedRef) => { const ref = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, ref) + const mergedRef = useMergedRefs(forwardedRef, ref) const {size, isVisibleChild} = React.useContext(ActionBarContext) const {groupId} = React.useContext(ActionBarGroupContext) @@ -507,7 +507,7 @@ export const ActionBarIconButton = forwardRef( return ( { const innerRef = React.useRef(null) - useRefObjectAsForwardedRef(forwardedRef, innerRef) + const mergedRef = useMergedRefs(forwardedRef, innerRef) const {headingId: headingId, variant: listVariant} = React.useContext(ListContext) const {container} = React.useContext(ActionListContainerContext) @@ -37,7 +37,7 @@ export const Heading = forwardRef(({as, size, children, visuallyHidden = false, (true) const {safeSetTimeout} = useSafeTimeout() @@ -160,7 +160,7 @@ const AutocompleteInput = React.forwardRef( onKeyDown={handleInputKeyDown} onKeyPress={onInputKeyPress} onKeyUp={handleInputKeyUp} - ref={inputRef} + ref={mergedRef} aria-controls={`${id}-listbox`} aria-autocomplete="both" role="combobox" diff --git a/packages/react/src/Autocomplete/AutocompleteOverlay.tsx b/packages/react/src/Autocomplete/AutocompleteOverlay.tsx index 0755434225e..f0486c5822d 100644 --- a/packages/react/src/Autocomplete/AutocompleteOverlay.tsx +++ b/packages/react/src/Autocomplete/AutocompleteOverlay.tsx @@ -5,7 +5,7 @@ import type {OverlayProps} from '../Overlay' import Overlay from '../Overlay' import type {ComponentProps} from '../utils/types' import {AutocompleteContext} from './AutocompleteContext' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' +import {useMergedRefs} from '../hooks/useMergedRefs' import VisuallyHidden from '../_VisuallyHidden' import classes from './AutocompleteOverlay.module.css' @@ -57,7 +57,7 @@ function AutocompleteOverlay({ [showMenu, selectedItemLength], ) - useRefObjectAsForwardedRef(scrollContainerRef, floatingElementRef) + const mergedScrollContainerRef = useMergedRefs(scrollContainerRef, floatingElementRef) const closeOptionList = useCallback(() => { setShowMenu(false) @@ -73,7 +73,7 @@ function AutocompleteOverlay({ preventFocusOnOpen={true} onClickOutside={closeOptionList} onEscape={closeOptionList} - ref={floatingElementRef as React.RefObject} + ref={mergedScrollContainerRef} top={position?.top} left={position?.left} className={clsx(classes.Overlay, className)} diff --git a/packages/react/src/Button/ButtonBase.tsx b/packages/react/src/Button/ButtonBase.tsx index 3aeeff2dc95..9e755912ab2 100644 --- a/packages/react/src/Button/ButtonBase.tsx +++ b/packages/react/src/Button/ButtonBase.tsx @@ -1,7 +1,7 @@ import React, {forwardRef, type JSX} from 'react' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import type {ButtonProps} from './types' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' +import {useMergedRefs} from '../hooks/useMergedRefs' import {VisuallyHidden} from '../VisuallyHidden' import Spinner from '../Spinner' import CounterLabel from '../CounterLabel' @@ -51,7 +51,7 @@ const ButtonBase = forwardRef(({children, as: Component = 'button', ...props}, f } = props const innerRef = React.useRef(null) - useRefObjectAsForwardedRef(forwardedRef, innerRef) + const mergedRef = useMergedRefs(forwardedRef, innerRef) const uuid = useId(id) const loadingAnnouncementID = `${uuid}-loading-announcement` @@ -89,7 +89,7 @@ const ButtonBase = forwardRef(({children, as: Component = 'button', ...props}, f data-component="Button" {...rest} // @ts-ignore temporary disable as we migrate to css modules, until we remove PolymorphicForwardRefComponent - ref={innerRef} + ref={mergedRef} className={clsx(classes.ButtonBase, className)} data-block={block ? 'block' : null} data-inactive={inactive ? true : undefined} diff --git a/packages/react/src/Dialog/Dialog.tsx b/packages/react/src/Dialog/Dialog.tsx index 449b9659292..2210951bd03 100644 --- a/packages/react/src/Dialog/Dialog.tsx +++ b/packages/react/src/Dialog/Dialog.tsx @@ -1,13 +1,12 @@ import React, {useCallback, useEffect, useRef, useState, type SyntheticEvent} from 'react' import type {ButtonProps} from '../Button' import {Button, IconButton} from '../Button' -import {useOnEscapePress, useProvidedRefOrCreate} from '../hooks' +import {useMergedRefs, useOnEscapePress, useProvidedRefOrCreate} from '../hooks' import {useFocusTrap} from '../hooks/useFocusTrap' import {XIcon} from '@primer/octicons-react' import {useFocusZone} from '../hooks/useFocusZone' import {FocusKeys} from '@primer/behaviors' import Portal from '../Portal' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' import {useId} from '../hooks/useId' import {ScrollableRegion} from '../ScrollableRegion' import type {ResponsiveValue} from '../hooks/useResponsiveValue' @@ -300,7 +299,7 @@ const _Dialog = React.forwardRef(null) - useRefObjectAsForwardedRef(forwardedRef, dialogRef) + const mergedDialogRef = useMergedRefs(forwardedRef, dialogRef) const backdropRef = useRef(null) useFocusTrap({ @@ -390,7 +389,7 @@ const _Dialog = React.forwardRef
{ const innerRef = React.useRef(null) - useRefObjectAsForwardedRef(forwardedRef, innerRef) + const mergedRef = useMergedRefs(forwardedRef, innerRef) if (__DEV__) { /** @@ -32,7 +32,7 @@ const Heading = forwardRef(({as: Component = 'h2', className, variant, ...props} }, [innerRef]) } - return + return }) as PolymorphicForwardRefComponent Heading.displayName = 'Heading' diff --git a/packages/react/src/Link/Link.tsx b/packages/react/src/Link/Link.tsx index 3e12db00c0b..4d088396455 100644 --- a/packages/react/src/Link/Link.tsx +++ b/packages/react/src/Link/Link.tsx @@ -1,6 +1,6 @@ import {clsx} from 'clsx' import React, {useEffect, type ForwardedRef, type ElementRef} from 'react' -import {useRefObjectAsForwardedRef} from '../hooks' +import {useMergedRefs} from '../hooks' import classes from './Link.module.css' import type {ComponentProps} from '../utils/types' import {type PolymorphicProps, fixedForwardRef} from '../utils/modern-polymorphic' @@ -20,7 +20,7 @@ export const UnwrappedLink = ( ) => { const {as: Component = 'a', className, inline, muted, hoverColor, ...restProps} = props const innerRef = React.useRef>(null) - useRefObjectAsForwardedRef(ref, innerRef) + const mergedRef = useMergedRefs(ref, innerRef) if (__DEV__) { /** @@ -55,7 +55,7 @@ export const UnwrappedLink = ( data-hover-color={hoverColor} {...restProps} // eslint-disable-next-line @typescript-eslint/no-explicit-any - ref={innerRef as any} + ref={mergedRef as any} /> ) } diff --git a/packages/react/src/Overlay/Overlay.tsx b/packages/react/src/Overlay/Overlay.tsx index 279690e1dff..3307973eb40 100644 --- a/packages/react/src/Overlay/Overlay.tsx +++ b/packages/react/src/Overlay/Overlay.tsx @@ -3,9 +3,8 @@ import React, {useEffect, useRef} from 'react' import useLayoutEffect from '../utils/useIsomorphicLayoutEffect' import type {AriaRole, Merge} from '../utils/types' import type {TouchOrMouseEvent} from '../hooks' -import {useOverlay} from '../hooks' +import {useMergedRefs, useOverlay} from '../hooks' import Portal from '../Portal' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' import type {AnchorSide} from '@primer/behaviors' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './Overlay.module.css' @@ -192,7 +191,7 @@ const Overlay = React.forwardRef( // eslint-disable-next-line @typescript-eslint/no-explicit-any ): ReactElement => { const overlayRef = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, overlayRef) + const mergedOverlayRef = useMergedRefs(forwardedRef, overlayRef) const slideAnimationDistance = 8 // var(--base-size-8), hardcoded to do some math const slideAnimationEasing = 'cubic-bezier(0.33, 1, 0.68, 1)' const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning') @@ -237,7 +236,7 @@ const Overlay = React.forwardRef( role={role} width={width} data-reflow-container={!preventOverflow ? true : undefined} - ref={overlayRef} + ref={mergedOverlayRef} left={leftPosition} right={right} height={height} diff --git a/packages/react/src/PageLayout/PageLayout.tsx b/packages/react/src/PageLayout/PageLayout.tsx index 97946a17f55..bd1ff29733a 100644 --- a/packages/react/src/PageLayout/PageLayout.tsx +++ b/packages/react/src/PageLayout/PageLayout.tsx @@ -1,7 +1,7 @@ import React, {memo, useRef} from 'react' import {clsx} from 'clsx' +import {useMergedRefs} from '../hooks' import {useId} from '../hooks/useId' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' import type {ResponsiveValue} from '../hooks/useResponsiveValue' import {isResponsiveValue} from '../hooks/useResponsiveValue' import {useSlots} from '../hooks/useSlots' @@ -838,7 +838,7 @@ const Pane = React.forwardRef
)}
(null) + const mergedRef = useMergedRefs(forwardedRef, ref) const selectedValuesDescriptionId = useId() - useRefObjectAsForwardedRef(forwardedRef, ref) const [selectedTokenIndex, setSelectedTokenIndex] = useState() const [tokensAreTruncated, setTokensAreTruncated] = useState(Boolean(visibleTokenCount)) const selectedTokenTexts = tokens @@ -314,7 +314,7 @@ function TextInputWithTokensInnerComponent
( ) => { const overlayRef = useRef(null) const modalRef = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, modalRef) + const mergedRef = useMergedRefs(forwardedRef, modalRef) const closeButtonRef = useRef(null) const onCloseClick = () => { @@ -73,7 +73,7 @@ const Dialog = forwardRef( Date: Tue, 12 May 2026 09:21:50 -0500 Subject: [PATCH 2/4] Delete .changeset/clean-ref-helper-usage.md --- .changeset/clean-ref-helper-usage.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/clean-ref-helper-usage.md diff --git a/.changeset/clean-ref-helper-usage.md b/.changeset/clean-ref-helper-usage.md deleted file mode 100644 index 0552e9e1bd7..00000000000 --- a/.changeset/clean-ref-helper-usage.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@primer/react': patch ---- - -Ref management: Replace internal `useRefObjectAsForwardedRef` usage with `useMergedRefs` From d86efe8fd0eac90ac0f17fcf4e7ded4da14aa2af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:48:29 +0000 Subject: [PATCH 3/4] Stabilize Autocomplete blur test in browser run Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- packages/react/src/Autocomplete/Autocomplete.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/react/src/Autocomplete/Autocomplete.test.tsx b/packages/react/src/Autocomplete/Autocomplete.test.tsx index 971b7b83ee8..6b98de67ba9 100644 --- a/packages/react/src/Autocomplete/Autocomplete.test.tsx +++ b/packages/react/src/Autocomplete/Autocomplete.test.tsx @@ -155,9 +155,10 @@ describe('Autocomplete', () => { expect(inputNode.getAttribute('aria-expanded')).toBe('true') - await userEvent.tab() + // eslint-disable-next-line github/no-blur + fireEvent.blur(inputNode) - expect(inputNode.getAttribute('aria-expanded')).not.toBe('true') + await waitFor(() => expect(inputNode.getAttribute('aria-expanded')).not.toBe('true')) }) it('sets the input value to the suggested item text and highlights the untyped part of the word', async () => { From 5c153b4d57a884b96d48de1ba23b2b9d5aee7dfc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 12 May 2026 14:49:50 +0000 Subject: [PATCH 4/4] Document deterministic blur event in Autocomplete test Co-authored-by: joshblack <3901764+joshblack@users.noreply.github.com> --- packages/react/src/Autocomplete/Autocomplete.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react/src/Autocomplete/Autocomplete.test.tsx b/packages/react/src/Autocomplete/Autocomplete.test.tsx index 6b98de67ba9..68e61313b3e 100644 --- a/packages/react/src/Autocomplete/Autocomplete.test.tsx +++ b/packages/react/src/Autocomplete/Autocomplete.test.tsx @@ -155,6 +155,7 @@ describe('Autocomplete', () => { expect(inputNode.getAttribute('aria-expanded')).toBe('true') + // `userEvent.tab()` is unreliable in browser-mode Vitest for this case; blur is deterministic. // eslint-disable-next-line github/no-blur fireEvent.blur(inputNode)