Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dcae302
Expose `useMergedRefs` and make ready for React 19
iansan5653 Mar 17, 2026
bb26a62
Add unit tests for `useMergedRefs`
iansan5653 Mar 17, 2026
3d854c9
Update import paths
iansan5653 Mar 17, 2026
8e77831
Deprecate `useProvidedRefOrCreate` and `useRefObjectAsForwardedRef`
iansan5653 Mar 17, 2026
973f184
Add changeset
iansan5653 Mar 17, 2026
63fca28
Deprecate `useProvidedRefOrCreate` and migrate everything except for …
iansan5653 Mar 9, 2026
3b478e3
Migrate `AnchoredOverlay` anchor ref
iansan5653 Mar 9, 2026
04e1ca7
Drop more ts-expect-error!
iansan5653 Mar 9, 2026
ec31af7
Revert behaviors docs
iansan5653 Mar 9, 2026
7bf1ce1
Update packages/react/src/Checkbox/Checkbox.tsx
iansan5653 Mar 10, 2026
006cc27
Update useCombinedRefs to useMergedRefs
iansan5653 Mar 17, 2026
12abb13
Merge branch 'main' of https://github.com/primer/react into replace-u…
iansan5653 Mar 24, 2026
196c083
Fix a few renames
iansan5653 Mar 24, 2026
6e410b6
Somehow I missed some spots???
iansan5653 Mar 24, 2026
a09ae22
anchorref > anchorRef
iansan5653 Mar 24, 2026
90e9941
Revert some files because of a valid use case
iansan5653 Mar 24, 2026
dda4382
Update `useProvidedRefOrCreate` to remove deprecation comment
iansan5653 Mar 24, 2026
f86b058
Don't return a callback ref in React 18-
iansan5653 Mar 24, 2026
c0612a7
Update tests
iansan5653 Mar 24, 2026
072e5b9
Merge branch 'main' into replace-use-provided-ref-or-create
iansan5653 Mar 31, 2026
44e6850
Fix double import from merging
iansan5653 Mar 31, 2026
08b8cfe
Revert changes to `Dialog`
iansan5653 Mar 31, 2026
3c00c5e
Merge branch 'main' into replace-use-provided-ref-or-create
iansan5653 Apr 1, 2026
a86ba2b
Merge branch 'main' into replace-use-provided-ref-or-create
francinelucca Apr 1, 2026
241bfdd
Revert "Revert changes to `Dialog`"
iansan5653 Apr 2, 2026
aeab544
Merge branch 'replace-use-provided-ref-or-create' of https://github.c…
iansan5653 Apr 2, 2026
a4a8707
Merge branch 'main' into replace-use-provided-ref-or-create
francinelucca Apr 3, 2026
d01c755
Try reverting `FilteredActionList`?
iansan5653 Apr 3, 2026
1a09d52
Revert "Try reverting `FilteredActionList`?"
iansan5653 Apr 3, 2026
0118db0
Let's try ActionList?
iansan5653 Apr 3, 2026
9f39ced
Revert "Let's try ActionList?"
iansan5653 Apr 3, 2026
07eeee3
Try PageHeader, TextInput, Tooltip
iansan5653 Apr 3, 2026
a120837
Revert "Try PageHeader, TextInput, Tooltip"
iansan5653 Apr 3, 2026
7b45d61
Just tooltip
iansan5653 Apr 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions packages/react/src/ActionList/List.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import React, {type JSX} from 'react'
import React, {useRef, type JSX} from 'react'
import {fixedForwardRef} from '../utils/modern-polymorphic'
import {ActionListContainerContext} from './ActionListContainerContext'
import {useSlots} from '../hooks/useSlots'
import {Heading} from './Heading'
import {useId} from '../hooks/useId'
import {ListContext, type ActionListProps} from './shared'
import {useProvidedRefOrCreate} from '../hooks'
import {useMergedRefs} from '../hooks'
import {FocusKeys, useFocusZone} from '../hooks/useFocusZone'
import {clsx} from 'clsx'
import classes from './ActionList.module.css'
Expand Down Expand Up @@ -41,7 +41,8 @@ const UnwrappedList = <As extends React.ElementType = 'ul'>(

const ariaLabelledBy = slots.heading ? (slots.heading.props.id ?? headingId) : listLabelledBy
const listRole = role || listRoleFromContainer
const listRef = useProvidedRefOrCreate(forwardedRef as React.RefObject<HTMLUListElement>)
const listRef = useRef<HTMLElement>(null)
const mergedRef = useMergedRefs(forwardedRef, listRef)

let enableFocusZone = false
if (enableFocusZoneFromContainer !== undefined) enableFocusZone = enableFocusZoneFromContainer
Expand Down Expand Up @@ -69,12 +70,11 @@ const UnwrappedList = <As extends React.ElementType = 'ul'>(
return (
<ListContext.Provider value={listContextValue}>
{slots.heading}
{/* @ts-expect-error ref needs a non nullable ref */}
<Component
className={clsx(classes.ActionList, className)}
role={listRole}
aria-labelledby={ariaLabelledBy}
ref={listRef}
ref={mergedRef}
data-dividers={showDividers}
data-variant={variant}
{...restProps}
Expand Down
21 changes: 3 additions & 18 deletions packages/react/src/AnchoredOverlay/AnchoredOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type {FocusTrapHookSettings} from '../hooks/useFocusTrap'
import {useFocusTrap} from '../hooks/useFocusTrap'
import type {FocusZoneHookSettings} from '../hooks/useFocusZone'
import {useFocusZone} from '../hooks/useFocusZone'
import {useAnchoredPosition, useProvidedRefOrCreate, useRenderForcingRef} from '../hooks'
import {useAnchoredPosition, useMergedRefs, useProvidedRefOrCreate, useRenderForcingRef} from '../hooks'
import {useId} from '../hooks/useId'
import type {AnchorPosition, PositionSettings} from '@primer/behaviors'
import {type ResponsiveValue} from '../hooks/useResponsiveValue'
Expand Down Expand Up @@ -176,6 +176,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
const cssAnchorPositioning = useFeatureFlag('primer_react_css_anchor_positioning')
const anchorRef = useProvidedRefOrCreate(externalAnchorRef)
const [overlayRef, updateOverlayRef] = useRenderForcingRef<HTMLDivElement>()
const mergedOverlayRef = useMergedRefs(updateOverlayRef, overlayProps?.ref)
const anchorId = useId(externalAnchorId)

const onClickOutside = useCallback(() => onClose?.('click-outside'), [onClose])
Expand Down Expand Up @@ -331,12 +332,7 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
{...(cssAnchorPositioning ? {popover: 'manual'} : {})}
{...restOverlayProps}
{...(cssAnchorPositioning ? {id: popoverId} : {})}
ref={node => {
if (overlayProps?.ref) {
assignRef(overlayProps.ref, node)
}
updateOverlayRef(node)
}}
ref={mergedOverlayRef}
data-anchor-position={cssAnchorPositioning}
data-side={cssAnchorPositioning ? side : position?.anchorSide}
>
Expand Down Expand Up @@ -365,15 +361,4 @@ export const AnchoredOverlay: React.FC<React.PropsWithChildren<AnchoredOverlayPr
)
}

function assignRef<T>(
ref: React.MutableRefObject<T | null> | ((instance: T | null) => void) | null | undefined,
value: T | null,
) {
if (typeof ref === 'function') {
ref(value)
} else if (ref) {
ref.current = value
}
}

AnchoredOverlay.displayName = 'AnchoredOverlay'
10 changes: 5 additions & 5 deletions packages/react/src/ButtonGroup/ButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {type PropsWithChildren} from 'react'
import React, {useRef, type PropsWithChildren} from 'react'
import classes from './ButtonGroup.module.css'
import {clsx} from 'clsx'
import {FocusKeys, useFocusZone} from '../hooks/useFocusZone'
import {useProvidedRefOrCreate} from '../hooks'
import {useMergedRefs} from '../hooks'
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'

export type ButtonGroupProps = PropsWithChildren<{
Expand All @@ -17,7 +17,8 @@ const ButtonGroup = React.forwardRef(function ButtonGroup(
forwardRef,
) {
const buttons = React.Children.map(children, (child, index) => <div key={index}>{child}</div>)
const buttonRef = useProvidedRefOrCreate(forwardRef as React.RefObject<HTMLDivElement | null>)
const buttonRef = useRef<HTMLDivElement>(null)
const mergedRef = useMergedRefs(buttonRef, forwardRef)

useFocusZone({
containerRef: buttonRef,
Expand All @@ -27,8 +28,7 @@ const ButtonGroup = React.forwardRef(function ButtonGroup(
})

return (
//@ts-expect-error it needs a non nullable ref
<BaseComponent ref={buttonRef} className={clsx(className, classes.ButtonGroup)} role={role} {...rest}>
<BaseComponent ref={mergedRef} className={clsx(className, classes.ButtonGroup)} role={role} {...rest}>
{buttons}
</BaseComponent>
)
Expand Down
18 changes: 13 additions & 5 deletions packages/react/src/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {clsx} from 'clsx'
import {useProvidedRefOrCreate} from '../hooks'
import React, {useContext, useEffect, type ChangeEventHandler, type InputHTMLAttributes, type ReactElement} from 'react'
import {useMergedRefs} from '../hooks'
import React, {
useContext,
useEffect,
useRef,
type ChangeEventHandler,
type InputHTMLAttributes,
type ReactElement,
} from 'react'
import useLayoutEffect from '../utils/useIsomorphicLayoutEffect'
import type {FormValidationStatus} from '../utils/types/FormValidationStatus'
import {CheckboxGroupContext} from '../CheckboxGroup/CheckboxGroupContext'
Expand Down Expand Up @@ -45,7 +52,8 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
ref,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): ReactElement<any> => {
const checkboxRef = useProvidedRefOrCreate(ref as React.RefObject<HTMLInputElement>)
const checkboxRef = useRef<HTMLInputElement>(null)
const mergedRef = useMergedRefs(checkboxRef, ref)
const checkboxGroupContext = useContext(CheckboxGroupContext)
const handleOnChange: ChangeEventHandler<HTMLInputElement> = e => {
checkboxGroupContext.onChange && checkboxGroupContext.onChange(e)
Expand All @@ -54,7 +62,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
const inputProps = {
type: 'checkbox',
disabled,
ref: checkboxRef,
ref: mergedRef,
checked: indeterminate ? false : checked,
defaultChecked,
required,
Expand Down Expand Up @@ -84,7 +92,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
checkbox.setAttribute('aria-checked', checkbox.checked ? 'true' : 'false')
}
})
// @ts-expect-error inputProp needs a non nullable ref

return <input {...inputProps} className={clsx(className, sharedClasses.Input, classes.Checkbox)} />
},
)
Expand Down
9 changes: 4 additions & 5 deletions packages/react/src/Dialog/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -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} 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 {useMergedRefs} from '../hooks/useMergedRefs'
import {useId} from '../hooks/useId'
import {ScrollableRegion} from '../ScrollableRegion'
import type {ResponsiveValue} from '../hooks/useResponsiveValue'
Expand Down Expand Up @@ -455,7 +454,8 @@ const Footer = React.forwardRef<HTMLDivElement, StyledFooterProps>(function Foot
Footer.displayName = 'Dialog.Footer'

const Buttons: React.FC<React.PropsWithChildren<{buttons: DialogButtonProps[]}>> = ({buttons}) => {
const autoFocusRef = useProvidedRefOrCreate<HTMLButtonElement>(buttons.find(button => button.autoFocus)?.ref)
const autoFocusRef = useRef<HTMLButtonElement>(null)
const mergedRef = useMergedRefs(autoFocusRef, buttons.find(button => button.autoFocus)?.ref)
let autoFocusCount = 0
const [hasRendered, setHasRendered] = useState(0)
useEffect(() => {
Expand All @@ -477,8 +477,7 @@ const Buttons: React.FC<React.PropsWithChildren<{buttons: DialogButtonProps[]}>>
{...buttonProps}
// 'normal' value is equivalent to 'default', this is used for backwards compatibility
variant={buttonType === 'normal' ? 'default' : buttonType}
// @ts-expect-error it needs a non nullable ref
ref={autoFocus && autoFocusCount === 0 ? (autoFocusCount++, autoFocusRef) : null}
ref={autoFocus && autoFocusCount === 0 ? (autoFocusCount++, mergedRef) : null}
>
{content}
</Button>
Expand Down
17 changes: 8 additions & 9 deletions packages/react/src/FilteredActionList/FilteredActionList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {ActionList, type ActionListProps} from '../ActionList'
import type {GroupedListProps, ListPropsBase, ItemInput, RenderItemFn} from './'
import {useFocusZone} from '../hooks/useFocusZone'
import {useId} from '../hooks/useId'
import {useProvidedRefOrCreate} from '../hooks/useProvidedRefOrCreate'
import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate'
import useScrollFlash from '../hooks/useScrollFlash'
import {VisuallyHidden} from '../VisuallyHidden'
Expand All @@ -22,6 +21,7 @@ import {isValidElementType} from 'react-is'
import {useAnnouncements} from './useAnnouncements'
import {clsx} from 'clsx'
import {useVirtualizer} from '@tanstack/react-virtual'
import {useMergedRefs} from '../hooks'

const menuScrollMargins: ScrollIntoViewOptions = {startMargin: 0, endMargin: 8}

Expand Down Expand Up @@ -189,10 +189,11 @@ export function FilteredActionList({
const inputAndListContainerRef = useRef<HTMLDivElement>(null)
const listRef = useRef<HTMLUListElement>(null)

const scrollContainerRef = useProvidedRefOrCreate<HTMLDivElement>(
providedScrollContainerRef as React.RefObject<HTMLDivElement>,
)
const inputRef = useProvidedRefOrCreate<HTMLInputElement>(providedInputRef)
const scrollContainerRef = useRef<HTMLDivElement>(null)
const combinedScrollContainerRef = useMergedRefs(scrollContainerRef, providedScrollContainerRef)

const inputRef = useRef<HTMLInputElement>(null)
const combinedInputRef = useMergedRefs(inputRef, providedInputRef)

const usingRovingTabindex = _PrivateFocusManagement === 'roving-tabindex'
const [listContainerElement, setListContainerElement] = useState<HTMLUListElement | null>(null)
Expand Down Expand Up @@ -548,8 +549,7 @@ export function FilteredActionList({
<div ref={inputAndListContainerRef} className={clsx(className, classes.Root)} data-testid="filtered-action-list">
<div className={classes.Header}>
<TextInput
// @ts-expect-error it needs a non nullable ref
ref={inputRef}
ref={combinedInputRef}
block
width="auto"
color="fg.default"
Expand Down Expand Up @@ -585,8 +585,7 @@ export function FilteredActionList({
</label>
</div>
)}
{/* @ts-expect-error div needs a non nullable ref */}
<div ref={scrollContainerRef} className={classes.Container}>
<div ref={combinedScrollContainerRef} className={classes.Container}>
{getBodyContent()}
</div>
</div>
Expand Down
13 changes: 6 additions & 7 deletions packages/react/src/PageHeader/PageHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect} from 'react'
import React, {useEffect, useRef} from 'react'
import type {ResponsiveValue} from '../hooks/useResponsiveValue'
import {isResponsiveValue} from '../hooks/useResponsiveValue'
import Heading from '../Heading'
Expand All @@ -10,7 +10,7 @@ import {getResponsiveAttributes} from '../internal/utils/getResponsiveAttributes
import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic'
import {areAllValuesTheSame, haveRegularAndWideSameValue} from '../utils/getBreakpointDeclarations'
import {warning} from '../utils/warning'
import {useProvidedRefOrCreate} from '../hooks'
import {useMergedRefs} from '../hooks'
import type {AriaRole, FCWithSlotMarker} from '../utils/types'
import {clsx} from 'clsx'

Expand Down Expand Up @@ -49,7 +49,8 @@ export type PageHeaderProps = {

const Root = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageHeaderProps>>(
({children, className, as: BaseComponent = 'div', 'aria-label': ariaLabel, role, hasBorder}, forwardedRef) => {
const rootRef = useProvidedRefOrCreate<HTMLDivElement>(forwardedRef as React.RefObject<HTMLDivElement>)
const rootRef = useRef<HTMLDivElement>(null)
const mergedRef = useMergedRefs(rootRef, forwardedRef)

const isInteractive = (element: HTMLElement) => {
return (
Expand Down Expand Up @@ -105,7 +106,7 @@ const Root = React.forwardRef<HTMLDivElement, React.PropsWithChildren<PageHeader

return (
<BaseComponent
ref={rootRef}
ref={mergedRef}
className={clsx(classes.PageHeader, className)}
data-has-border={hasBorder ? 'true' : undefined}
aria-label={ariaLabel}
Expand Down Expand Up @@ -205,12 +206,10 @@ export type TitleAreaProps = {

const TitleArea = React.forwardRef<HTMLDivElement, React.PropsWithChildren<TitleAreaProps>>(
({children, className, hidden = false, variant = 'medium'}, forwardedRef) => {
const titleAreaRef = useProvidedRefOrCreate<HTMLDivElement>(forwardedRef as React.RefObject<HTMLDivElement>)
return (
<div
className={clsx(classes.TitleArea, className)}
// @ts-expect-error it needs a non nullable ref
ref={titleAreaRef}
ref={forwardedRef}
data-component="TitleArea"
{...getResponsiveAttributes('size-variant', variant)}
{...getHiddenDataAttributes(hidden)}
Expand Down
8 changes: 4 additions & 4 deletions packages/react/src/TextInput/TextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {AlertFillIcon} from '@primer/octicons-react'

import classes from './TextInput.module.css'
import TextInputInnerVisualSlot from '../internal/components/TextInputInnerVisualSlot'
import {useProvidedRefOrCreate} from '../hooks'
import type {Merge} from '../utils/types'
import type {StyledWrapperProps} from '../internal/components/TextInputWrapper'
import TextInputWrapper from '../internal/components/TextInputWrapper'
Expand All @@ -16,6 +15,7 @@ import UnstyledTextInput from '../internal/components/UnstyledTextInput'
import VisuallyHidden from '../_VisuallyHidden'
import {CharacterCounter} from '../utils/character-counter'
import Text from '../Text'
import {useMergedRefs} from '../hooks'

export type TextInputNonPassthroughProps = {
/** @deprecated Use `leadingVisual` or `trailingVisual` prop instead */
Expand Down Expand Up @@ -103,7 +103,8 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
ref,
) => {
const [isInputFocused, setIsInputFocused] = useState<boolean>(false)
const inputRef = useProvidedRefOrCreate(ref as React.RefObject<HTMLInputElement | null>)
const inputRef = useRef<HTMLInputElement>(null)
const mergedRef = useMergedRefs(inputRef, ref)
const [characterCount, setCharacterCount] = useState<string>('')
const [isOverLimit, setIsOverLimit] = useState<boolean>(false)
const [screenReaderMessage, setScreenReaderMessage] = useState<string>('')
Expand Down Expand Up @@ -258,8 +259,7 @@ const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
{typeof LeadingVisual !== 'string' && isValidElementType(LeadingVisual) ? <LeadingVisual /> : LeadingVisual}
</TextInputInnerVisualSlot>
<UnstyledTextInput
// @ts-expect-error it needs a non nullable ref
ref={inputRef}
ref={mergedRef}
disabled={disabled}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const CustomTabList = (props: React.PropsWithChildren) => {

return (
<div style={{width: '200px'}}>
{/* @ts-expect-error it needs a non nullable ref */}
<ActionList {...tabListProps}>{props.children}</ActionList>
</div>
)
Expand Down
11 changes: 6 additions & 5 deletions packages/react/src/experimental/Tabs/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import React, {
useContext,
useId,
useMemo,
useRef,
type AriaAttributes,
type ElementRef,
type PropsWithChildren,
} from 'react'
import useIsomorphicLayoutEffect from '../../utils/useIsomorphicLayoutEffect'
import {useControllableState} from '../../hooks/useControllableState'
import {useProvidedRefOrCreate} from '../../hooks'
import {useMergedRefs} from '../../hooks'

/**
* Props to be used when the Tabs component's state is controlled by the parent
Expand Down Expand Up @@ -114,13 +115,14 @@ function useTabList<T extends HTMLElement>(
'aria-orientation': AriaAttributes['aria-orientation']
'aria-label': AriaAttributes['aria-label']
'aria-labelledby': AriaAttributes['aria-labelledby']
ref: React.RefObject<T | null>
ref: React.Ref<T>
role: 'tablist'
}
} {
const {'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-orientation': ariaOrientation} = props

const ref = useProvidedRefOrCreate(props.ref)
const ref = useRef<T>(null)
const mergedRef = useMergedRefs(ref, props.ref)

const onKeyDown = (event: React.KeyboardEvent) => {
const {current: tablist} = ref
Expand Down Expand Up @@ -172,7 +174,7 @@ function useTabList<T extends HTMLElement>(

return {
tabListProps: {
ref,
ref: mergedRef,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledby,
'aria-orientation': ariaOrientation ?? 'horizontal',
Expand All @@ -186,7 +188,6 @@ function TabList({children, ...rest}: TabListProps) {
const {tabListProps} = useTabList<HTMLDivElement>(rest)

return (
// @ts-expect-error it needs a non nullable ref
<div {...rest} {...tabListProps}>
{children}
</div>
Expand Down
Loading
Loading