diff --git a/.changeset/combined-refs-hook.md b/.changeset/combined-refs-hook.md new file mode 100644 index 00000000000..e8e9c96e0cc --- /dev/null +++ b/.changeset/combined-refs-hook.md @@ -0,0 +1,5 @@ +--- +'@primer/react': patch +--- + +Update internal implementations of combined refs to improve performance and add support for React 19 callback refs diff --git a/packages/react/src/ActionBar/ActionBar.tsx b/packages/react/src/ActionBar/ActionBar.tsx index 83712ce21fd..2ba3b086b9c 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(ref, forwardedRef) 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, { const inputNode = getByLabelText(AUTOCOMPLETE_LABEL) expect(inputNode.getAttribute('aria-expanded')).not.toBe('true') + inputNode.focus() fireEvent.click(inputNode) fireEvent.keyDown(inputNode, {key: 'ArrowDown'}) diff --git a/packages/react/src/Autocomplete/AutocompleteInput.tsx b/packages/react/src/Autocomplete/AutocompleteInput.tsx index 61b2905e5ba..ef1278451a1 100644 --- a/packages/react/src/Autocomplete/AutocompleteInput.tsx +++ b/packages/react/src/Autocomplete/AutocompleteInput.tsx @@ -3,7 +3,7 @@ import React, {useCallback, useContext, useEffect, useState} from 'react' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import {AutocompleteContext, AutocompleteInputContext} from './AutocompleteContext' import TextInput from '../TextInput' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' +import {useMergedRefs} from '../hooks/useMergedRefs' import type {ComponentProps} from '../utils/types' import useSafeTimeout from '../hooks/useSafeTimeout' @@ -43,7 +43,7 @@ const AutocompleteInput = React.forwardRef( } const {activeDescendantRef, id, inputRef, setInputValue, setShowMenu, showMenu} = autocompleteContext const {autocompleteSuggestion = '', inputValue = '', isMenuDirectlyActivated} = inputContext - useRefObjectAsForwardedRef(forwardedRef, inputRef) + const mergedRef = useMergedRefs(forwardedRef, inputRef) const [highlightRemainingText, setHighlightRemainingText] = useState(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..9d810b90f75 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 mergedRef = 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={mergedRef} 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 aa337e32ea7..e1860cae324 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 combinedRefs = useMergedRefs(forwardedRef, innerRef) const uuid = useId(id) const loadingAnnouncementID = `${uuid}-loading-announcement` @@ -87,8 +87,7 @@ const ButtonBase = forwardRef(({children, as: Component = 'button', ...props}, f (null) - useRefObjectAsForwardedRef(forwardedRef, dialogRef) + const mergedRef = useMergedRefs(forwardedRef, dialogRef) const backdropRef = useRef(null) useFocusTrap({ @@ -362,7 +362,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 1680afb2eb1..d57013dc36d 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, hoverColor, ...restProps} = props const innerRef = React.useRef>(null) - useRefObjectAsForwardedRef(ref, innerRef) + const mergedRef = useMergedRefs(ref, innerRef) if (__DEV__) { /** @@ -53,8 +53,7 @@ export const UnwrappedLink = ( data-inline={inline} data-hover-color={hoverColor} {...restProps} - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ref={innerRef as any} + ref={mergedRef} /> ) } diff --git a/packages/react/src/Overlay/Overlay.tsx b/packages/react/src/Overlay/Overlay.tsx index ae39f51ceec..a959a087249 100644 --- a/packages/react/src/Overlay/Overlay.tsx +++ b/packages/react/src/Overlay/Overlay.tsx @@ -5,7 +5,7 @@ import type {AriaRole, Merge} from '../utils/types' import type {TouchOrMouseEvent} from '../hooks' import {useOverlay} from '../hooks' import Portal from '../Portal' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' +import {useMergedRefs} from '../hooks/useMergedRefs' import type {AnchorSide} from '@primer/behaviors' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './Overlay.module.css' @@ -193,7 +193,7 @@ const Overlay = React.forwardRef( const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning') const featureFlagMaxHeightClampToViewport = useFeatureFlag('primer_react_overlay_max_height_clamp_to_viewport') const overlayRef = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, overlayRef) + const mergedRef = 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)' @@ -237,7 +237,7 @@ const Overlay = React.forwardRef( role={role} width={width} data-reflow-container={!preventOverflow ? true : undefined} - ref={overlayRef} + ref={mergedRef} left={leftPosition} right={right} height={height} diff --git a/packages/react/src/PageLayout/PageLayout.tsx b/packages/react/src/PageLayout/PageLayout.tsx index 97946a17f55..0cee74e68a4 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 {useId} from '../hooks/useId' -import {useRefObjectAsForwardedRef} from '../hooks/useRefObjectAsForwardedRef' +import {useMergedRefs} from '../hooks/useMergedRefs' 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 selectedValuesDescriptionId = useId() - useRefObjectAsForwardedRef(forwardedRef, ref) + const mergedRef = useMergedRefs(forwardedRef, ref) const [selectedTokenIndex, setSelectedTokenIndex] = useState() const [tokensAreTruncated, setTokensAreTruncated] = useState(Boolean(visibleTokenCount)) const selectedTokenTexts = tokens @@ -310,7 +310,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( const Component = forwardRef(({asButton}, forwardedRef) => { const ref: InputOrButtonRef = React.useRef(null) - const combinedRef = useMergedRefs(forwardedRef, ref) + const mergedRef = useMergedRefs(forwardedRef, ref) - return asButton ?