diff --git a/.changeset/combined-refs-hook.md b/.changeset/combined-refs-hook.md new file mode 100644 index 00000000000..75836b99950 --- /dev/null +++ b/.changeset/combined-refs-hook.md @@ -0,0 +1,5 @@ +--- +'@primer/react': minor +--- + +Add `useCombinedRefs` hook and deprecate `useRefObjectAsForwardedRef` diff --git a/packages/react/src/Autocomplete/Autocomplete.test.tsx b/packages/react/src/Autocomplete/Autocomplete.test.tsx index 971b7b83ee8..eb8f603ad30 100644 --- a/packages/react/src/Autocomplete/Autocomplete.test.tsx +++ b/packages/react/src/Autocomplete/Autocomplete.test.tsx @@ -150,6 +150,7 @@ describe('Autocomplete', () => { 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..3bcac66dd80 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 {useCombinedRefs} from '../hooks/useCombinedRefs' 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 combinedRef = useCombinedRefs(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={combinedRef} 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..203b4e6bcde 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 {useCombinedRefs} from '../hooks/useCombinedRefs' import VisuallyHidden from '../_VisuallyHidden' import classes from './AutocompleteOverlay.module.css' @@ -57,7 +57,7 @@ function AutocompleteOverlay({ [showMenu, selectedItemLength], ) - useRefObjectAsForwardedRef(scrollContainerRef, floatingElementRef) + const combinedRef = useCombinedRefs(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={combinedRef} 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..4069647a934 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 {useCombinedRefs} from '../hooks/useCombinedRefs' 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 = useCombinedRefs(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 combinedRef = useCombinedRefs(forwardedRef, dialogRef) const backdropRef = useRef(null) useFocusTrap({ @@ -361,7 +361,7 @@ const _Dialog = React.forwardRef
{ const innerRef = React.useRef(null) - useRefObjectAsForwardedRef(forwardedRef, innerRef) + const combinedRef = useCombinedRefs(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..c4f16e0c90b 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 {useCombinedRefs} 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 combinedRef = useCombinedRefs(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={combinedRef} /> ) } diff --git a/packages/react/src/Overlay/Overlay.tsx b/packages/react/src/Overlay/Overlay.tsx index 362083945e2..6c9e67568b8 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 {useCombinedRefs} from '../hooks/useCombinedRefs' import type {AnchorSide} from '@primer/behaviors' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' import classes from './Overlay.module.css' @@ -190,7 +190,7 @@ const Overlay = React.forwardRef( // eslint-disable-next-line @typescript-eslint/no-explicit-any ): ReactElement => { const overlayRef = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, overlayRef) + const combinedRef = useCombinedRefs(forwardedRef, overlayRef) const slideAnimationDistance = 8 // var(--base-size-8), hardcoded to do some math const slideAnimationEasing = 'cubic-bezier(0.33, 1, 0.68, 1)' @@ -235,7 +235,7 @@ const Overlay = React.forwardRef( role={role} width={width} data-reflow-container={!preventOverflow ? true : undefined} - ref={overlayRef} + ref={combinedRef} left={leftPosition} right={right} height={height} diff --git a/packages/react/src/PageLayout/PageLayout.tsx b/packages/react/src/PageLayout/PageLayout.tsx index 1120e8c6895..cde829e9e41 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 {useCombinedRefs} from '../hooks/useCombinedRefs' import type {ResponsiveValue} from '../hooks/useResponsiveValue' import {isResponsiveValue} from '../hooks/useResponsiveValue' import {useSlots} from '../hooks/useSlots' @@ -868,7 +868,7 @@ const Pane = React.forwardRef
)}
(null) const selectedValuesDescriptionId = useId() - useRefObjectAsForwardedRef(forwardedRef, ref) + const combinedRef = useCombinedRefs(forwardedRef, ref) const [selectedTokenIndex, setSelectedTokenIndex] = useState() const [tokensAreTruncated, setTokensAreTruncated] = useState(Boolean(visibleTokenCount)) const selectedTokenTexts = tokens @@ -310,7 +310,7 @@ function TextInputWithTokensInnerComponent
should not update exports without a semver change 1`] = "type UnderlineNavProps", "useAnchoredPosition", "useColorSchemeVar", + "useCombinedRefs", "useConfirm", "useDetails", "useFocusTrap", diff --git a/packages/react/src/deprecated/DialogV1/Dialog.tsx b/packages/react/src/deprecated/DialogV1/Dialog.tsx index caad7a4ab2c..c0f26868652 100644 --- a/packages/react/src/deprecated/DialogV1/Dialog.tsx +++ b/packages/react/src/deprecated/DialogV1/Dialog.tsx @@ -2,7 +2,7 @@ import React, {forwardRef, useRef, type HTMLAttributes} from 'react' import {IconButton} from '../../Button' import useDialog from '../../hooks/useDialog' import type {ComponentProps} from '../../utils/types' -import {useRefObjectAsForwardedRef} from '../../hooks/useRefObjectAsForwardedRef' +import {useCombinedRefs} from '../../hooks/useCombinedRefs' import {XIcon} from '@primer/octicons-react' import {clsx} from 'clsx' import classes from './Dialog.module.css' @@ -48,7 +48,7 @@ const Dialog = forwardRef( ) => { const overlayRef = useRef(null) const modalRef = useRef(null) - useRefObjectAsForwardedRef(forwardedRef, modalRef) + const combinedRef = useCombinedRefs(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 = useCombinedRefs(forwardedRef, ref) + + return asButton ?