diff --git a/.changeset/early-ravens-visit.md b/.changeset/early-ravens-visit.md new file mode 100644 index 00000000000..f9dc700a5d4 --- /dev/null +++ b/.changeset/early-ravens-visit.md @@ -0,0 +1,7 @@ +--- +"@primer/react": minor +--- + +- New: Exposes new `useMergedRefs` hook that can merge two refs into a single combined ref +- Deprecates `useRefObjectAsForwardedRef`; see doc comment for migration instructions +- Deprecates `useProvidedRefOrCreate`; see doc comment for migration instructions diff --git a/packages/react/src/ActionList/List.tsx b/packages/react/src/ActionList/List.tsx index ee9213fcb50..53cf3ede473 100644 --- a/packages/react/src/ActionList/List.tsx +++ b/packages/react/src/ActionList/List.tsx @@ -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' @@ -41,7 +41,8 @@ const UnwrappedList = ( const ariaLabelledBy = slots.heading ? (slots.heading.props.id ?? headingId) : listLabelledBy const listRole = role || listRoleFromContainer - const listRef = useProvidedRefOrCreate(forwardedRef as React.RefObject) + const listRef = useRef(null) + const mergedRef = useMergedRefs(forwardedRef, listRef) let enableFocusZone = false if (enableFocusZoneFromContainer !== undefined) enableFocusZone = enableFocusZoneFromContainer @@ -69,12 +70,11 @@ const UnwrappedList = ( return ( {slots.heading} - {/* @ts-expect-error ref needs a non nullable ref */} > = ({ ) const menuButtonChildId = React.isValidElement(menuButtonChild) ? menuButtonChild.props.id : undefined - const anchorRef = useProvidedRefOrCreate(externalAnchorRef) + const anchorRef = useRef(null) + const mergedRef = useMergedRefs(anchorRef, externalAnchorRef) + const anchorId = useId(menuButtonChildId) let renderAnchor: AnchoredOverlayProps['renderAnchor'] = null // 🚨 Hack for good API! @@ -130,7 +132,7 @@ const Menu: FCWithSlotMarker> = ({ anchorChildren, mergeAnchorHandlers({...anchorProps}, anchorChildren.props), ) - return React.cloneElement(child, {children: triggerButton, ref: anchorRef}) + return React.cloneElement(child, {children: triggerButton, ref: combinedRef}) } } return null @@ -149,7 +151,7 @@ const Menu: FCWithSlotMarker> = ({ mergeAnchorHandlers({...anchorProps}, tooltipTrigger.props), ) const tooltip = React.cloneElement(anchorChildren, {children: tooltipTriggerEl}) - return React.cloneElement(child, {children: tooltip, ref: anchorRef}) + return React.cloneElement(child, {children: tooltip, ref: combinedRef}) } } } else { @@ -278,7 +280,7 @@ const Overlay: FCWithSlotMarker> = ({ // we typecast anchorRef as required instead of optional // because we know that we're setting it in context in Menu const { - anchorRef, + anchorRef: contextAnchorRef, renderAnchor, anchorId, open, @@ -287,6 +289,9 @@ const Overlay: FCWithSlotMarker> = ({ isSubmenu = false, } = React.useContext(MenuContext) as MandateProps + const anchorRef = useRef(null) + const combinedAnchorRef = useMergedRefs(anchorRef, contextAnchorRef) + const containerRef = React.useRef(null) const isNarrow = useResponsiveValue({narrow: true}, false) @@ -328,7 +333,7 @@ const Overlay: FCWithSlotMarker> = ({ return ( + anchorRef?: React.Ref /** * An override to the internal id that will be spread on to the renderAnchor @@ -46,7 +46,7 @@ interface AnchoredOverlayPropsWithoutAnchor { * An override to the internal renderAnchor ref that will be used to position the overlay. * When renderAnchor is null this can be used to make an anchor that is detached from ActionMenu. */ - anchorRef: React.RefObject + anchorRef: React.Ref /** * An override to the internal id that will be spread on to the renderAnchor */ @@ -160,8 +160,12 @@ export const AnchoredOverlay: React.FC { - const anchorRef = useProvidedRefOrCreate(externalAnchorRef) + const anchorRef = useRef(null) + const mergedRef = useMergedRefs(anchorRef, externalAnchorRef) + const [overlayRef, updateOverlayRef] = useRenderForcingRef() + const combinedOverlayRef = useMergedRefs(updateOverlayRef, overlayProps?.ref) + const anchorId = useId(externalAnchorId) const onClickOutside = useCallback(() => onClose?.('click-outside'), [onClose]) @@ -235,7 +239,7 @@ export const AnchoredOverlay: React.FC {renderAnchor && renderAnchor({ - ref: anchorRef, + ref: combinedRef, id: anchorId, 'aria-haspopup': 'true', 'aria-expanded': open, @@ -261,12 +265,7 @@ export const AnchoredOverlay: React.FC { - if (overlayProps?.ref) { - assignRef(overlayProps.ref, node) - } - updateOverlayRef(node) - }} + ref={combinedOverlayRef} > {showXIcon ? (
@@ -293,15 +292,4 @@ export const AnchoredOverlay: React.FC( - ref: React.MutableRefObject | ((instance: T | null) => void) | null | undefined, - value: T | null, -) { - if (typeof ref === 'function') { - ref(value) - } else if (ref) { - ref.current = value - } -} - AnchoredOverlay.displayName = 'AnchoredOverlay' diff --git a/packages/react/src/Banner/Banner.tsx b/packages/react/src/Banner/Banner.tsx index 3b5a2eecaaf..44db1cbd3fb 100644 --- a/packages/react/src/Banner/Banner.tsx +++ b/packages/react/src/Banner/Banner.tsx @@ -3,7 +3,7 @@ import React, {forwardRef, useEffect} from 'react' import {AlertIcon, InfoIcon, StopIcon, CheckCircleIcon, XIcon} from '@primer/octicons-react' import {Button, IconButton, type ButtonProps} from '../Button' import {VisuallyHidden} from '../VisuallyHidden' -import {useMergedRefs} from '../internal/hooks/useMergedRefs' +import {useMergedRefs} from '../hooks/useMergedRefs' import {useId} from '../hooks/useId' import classes from './Banner.module.css' import type {ForwardRefComponent as PolymorphicForwardRefComponent} from '../utils/polymorphic' diff --git a/packages/react/src/ButtonGroup/ButtonGroup.tsx b/packages/react/src/ButtonGroup/ButtonGroup.tsx index eff345f616b..1dfe82dc0f4 100644 --- a/packages/react/src/ButtonGroup/ButtonGroup.tsx +++ b/packages/react/src/ButtonGroup/ButtonGroup.tsx @@ -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<{ @@ -17,7 +17,8 @@ const ButtonGroup = React.forwardRef(function ButtonGroup( forwardRef, ) { const buttons = React.Children.map(children, (child, index) =>
{child}
) - const buttonRef = useProvidedRefOrCreate(forwardRef as React.RefObject) + const buttonRef = useRef(null) + const mergedRef = useMergedRefs(buttonRef, forwardRef) useFocusZone({ containerRef: buttonRef, @@ -27,8 +28,7 @@ const ButtonGroup = React.forwardRef(function ButtonGroup( }) return ( - //@ts-expect-error it needs a non nullable ref - + {buttons} ) diff --git a/packages/react/src/Checkbox/Checkbox.tsx b/packages/react/src/Checkbox/Checkbox.tsx index c992f840ebb..8b0805b2def 100644 --- a/packages/react/src/Checkbox/Checkbox.tsx +++ b/packages/react/src/Checkbox/Checkbox.tsx @@ -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' @@ -45,7 +52,8 @@ const Checkbox = React.forwardRef( ref, // eslint-disable-next-line @typescript-eslint/no-explicit-any ): ReactElement => { - const checkboxRef = useProvidedRefOrCreate(ref as React.RefObject) + const checkboxRef = useRef(null) + const mergedRef = useMergedRefs(checkboxRef, ref) const checkboxGroupContext = useContext(CheckboxGroupContext) const handleOnChange: ChangeEventHandler = e => { checkboxGroupContext.onChange && checkboxGroupContext.onChange(e) @@ -54,7 +62,7 @@ const Checkbox = React.forwardRef( const inputProps = { type: 'checkbox', disabled, - ref: checkboxRef, + ref: combinedRef, checked: indeterminate ? false : checked, defaultChecked, required, @@ -84,7 +92,7 @@ const Checkbox = React.forwardRef( checkbox.setAttribute('aria-checked', checkbox.checked ? 'true' : 'false') } }) - // @ts-expect-error inputProp needs a non nullable ref + return }, ) diff --git a/packages/react/src/Details/Details.tsx b/packages/react/src/Details/Details.tsx index c9135b2db31..2d512c41dfe 100644 --- a/packages/react/src/Details/Details.tsx +++ b/packages/react/src/Details/Details.tsx @@ -2,7 +2,7 @@ import React, {useEffect, type ComponentPropsWithoutRef, type ReactElement} from import {warning} from '../utils/warning' import {clsx} from 'clsx' import classes from './Details.module.css' -import {useMergedRefs} from '../internal/hooks/useMergedRefs' +import {useMergedRefs} from '../hooks/useMergedRefs' const Root = React.forwardRef( // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/react/src/Dialog/Dialog.tsx b/packages/react/src/Dialog/Dialog.tsx index 4ef73390514..81cd3b21b23 100644 --- a/packages/react/src/Dialog/Dialog.tsx +++ b/packages/react/src/Dialog/Dialog.tsx @@ -1,7 +1,7 @@ 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 {useOnEscapePress} from '../hooks' import {useFocusTrap} from '../hooks/useFocusTrap' import {XIcon} from '@primer/octicons-react' import {useFocusZone} from '../hooks/useFocusZone' @@ -428,7 +428,8 @@ const Footer = React.forwardRef(function Foot Footer.displayName = 'Dialog.Footer' const Buttons: React.FC> = ({buttons}) => { - const autoFocusRef = useProvidedRefOrCreate(buttons.find(button => button.autoFocus)?.ref) + const autoFocusRef = useRef(null) + const mergedRef = useMergedRefs(autoFocusRef, buttons.find(button => button.autoFocus)?.ref) let autoFocusCount = 0 const [hasRendered, setHasRendered] = useState(0) useEffect(() => { @@ -450,8 +451,7 @@ const Buttons: React.FC> {...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++, combinedRef) : null} > {content} diff --git a/packages/react/src/FilteredActionList/FilteredActionList.tsx b/packages/react/src/FilteredActionList/FilteredActionList.tsx index 10afa57107c..94d4481599e 100644 --- a/packages/react/src/FilteredActionList/FilteredActionList.tsx +++ b/packages/react/src/FilteredActionList/FilteredActionList.tsx @@ -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' @@ -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} @@ -189,10 +189,11 @@ export function FilteredActionList({ const inputAndListContainerRef = useRef(null) const listRef = useRef(null) - const scrollContainerRef = useProvidedRefOrCreate( - providedScrollContainerRef as React.RefObject, - ) - const inputRef = useProvidedRefOrCreate(providedInputRef) + const scrollContainerRef = useRef(null) + const combinedScrollContainerRef = useMergedRefs(scrollContainerRef, providedScrollContainerRef) + + const inputRef = useRef(null) + const combinedInputRef = useMergedRefs(inputRef, providedInputRef) const usingRovingTabindex = _PrivateFocusManagement === 'roving-tabindex' const [listContainerElement, setListContainerElement] = useState(null) @@ -548,8 +549,7 @@ export function FilteredActionList({
)} - {/* @ts-expect-error div needs a non nullable ref */} -
+
{getBodyContent()}
diff --git a/packages/react/src/PageHeader/PageHeader.tsx b/packages/react/src/PageHeader/PageHeader.tsx index 1acaadf06f3..fa3095f86aa 100644 --- a/packages/react/src/PageHeader/PageHeader.tsx +++ b/packages/react/src/PageHeader/PageHeader.tsx @@ -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' @@ -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' @@ -49,7 +49,8 @@ export type PageHeaderProps = { const Root = React.forwardRef>( ({children, className, as: BaseComponent = 'div', 'aria-label': ariaLabel, role, hasBorder}, forwardedRef) => { - const rootRef = useProvidedRefOrCreate(forwardedRef as React.RefObject) + const rootRef = useRef(null) + const mergedRef = useMergedRefs(rootRef, forwardedRef) const isInteractive = (element: HTMLElement) => { return ( @@ -105,7 +106,7 @@ const Root = React.forwardRef>( ({children, className, hidden = false, variant = 'medium'}, forwardedRef) => { - const titleAreaRef = useProvidedRefOrCreate(forwardedRef as React.RefObject) return (
(null) + const combinedAnchorRef = useMergedRefs(anchorRef, externalAnchorRef) + const onOpen: AnchoredOverlayProps['onOpen'] = useCallback( (gesture: Parameters>[0]) => onOpenChange(true, gesture), [onOpenChange], @@ -860,7 +862,7 @@ function Panel({ <> ( ref, ) => { const [isInputFocused, setIsInputFocused] = useState(false) - const inputRef = useProvidedRefOrCreate(ref as React.RefObject) + const inputRef = useRef(null) + const mergedRef = useMergedRefs(inputRef, ref) const [characterCount, setCharacterCount] = useState('') const [isOverLimit, setIsOverLimit] = useState(false) const [screenReaderMessage, setScreenReaderMessage] = useState('') @@ -258,8 +259,7 @@ const TextInput = React.forwardRef( {typeof LeadingVisual !== 'string' && isValidElementType(LeadingVisual) ? : LeadingVisual} & { - ref?: React.RefObject + ref?: React.Ref } // map tooltip direction to anchoredPosition props @@ -124,7 +124,8 @@ export const Tooltip: ForwardRefExoticComponent< ) => { const tooltipId = useId(id) const child = Children.only(children) - const triggerRef = useProvidedRefOrCreate(forwardedRef as React.RefObject) + const triggerRef = useRef(null) + const combinedTriggerRef = useMergedRefs(triggerRef, forwardedRef) const tooltipElRef = useRef(null) const [calculatedDirection, setCalculatedDirection] = useState(direction) @@ -279,8 +280,7 @@ export const Tooltip: ForwardRefExoticComponent< {React.isValidElement(child) && // eslint-disable-next-line react-hooks/refs React.cloneElement(child as React.ReactElement, { - // @ts-expect-error it needs a non nullable ref - ref: triggerRef, + ref: combinedTriggerRef, // If it is a type description, we use tooltip to describe the trigger 'aria-describedby': (() => { // If tooltip is not a description type, keep the original aria-describedby diff --git a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap index 77f3b55f2a2..cec11bdd137 100644 --- a/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/react/src/__tests__/__snapshots__/exports.test.ts.snap @@ -218,6 +218,7 @@ exports[`@primer/react > should not update exports without a semver change 1`] = "useFormControlForwardedProps", "useId", "useIsomorphicLayoutEffect", + "useMergedRefs", "useOnEscapePress", "useOnOutsideClick", "useOpenAndCloseFocus", diff --git a/packages/react/src/deprecated/ActionMenu.tsx b/packages/react/src/deprecated/ActionMenu.tsx index fa0cf1de949..65343d1318b 100644 --- a/packages/react/src/deprecated/ActionMenu.tsx +++ b/packages/react/src/deprecated/ActionMenu.tsx @@ -6,11 +6,11 @@ import {Divider} from './ActionList/Divider' import type {ButtonProps} from '../Button' import {Button} from '../Button' import type React from 'react' -import {useCallback, useMemo, type JSX} from 'react' +import {useCallback, useMemo, useRef, type JSX} from 'react' import {AnchoredOverlay} from '../AnchoredOverlay' import {useProvidedStateOrCreate} from '../hooks/useProvidedStateOrCreate' import type {OverlayProps} from '../Overlay' -import {useProvidedRefOrCreate} from '../hooks' +import {useMergedRefs} from '../hooks' import type {AnchoredOverlayWrapperAnchorProps} from '../AnchoredOverlay/AnchoredOverlay' interface ActionMenuBaseProps extends Partial>, ListPropsBase { @@ -60,7 +60,8 @@ const ActionMenuBase = ({ ...listProps }: ActionMenuProps): JSX.Element => { const [combinedOpenState, setCombinedOpenState] = useProvidedStateOrCreate(open, setOpen, false) - const anchorRef = useProvidedRefOrCreate(externalAnchorRef) + const anchorRef = useRef(null) + const mergedRef = useMergedRefs(anchorRef, externalAnchorRef) const onOpen = useCallback(() => setCombinedOpenState(true), [setCombinedOpenState]) const onClose = useCallback(() => setCombinedOpenState(false), [setCombinedOpenState]) @@ -96,7 +97,7 @@ const ActionMenuBase = ({ return ( = ({ // with additional props for accessibility // eslint-disable-next-line @typescript-eslint/no-explicit-any let Anchor: React.ReactElement | undefined - const anchorRef = useProvidedRefOrCreate(providedAnchorRef) + + const anchorRef = useRef(null) + const mergedRef = useMergedRefs(providedAnchorRef, anchorRef) const onAnchorClick = () => { if (!internalOpen) setInternalOpen(true) @@ -130,7 +132,7 @@ const Panel: React.FC = ({ // eslint-disable-next-line react-hooks/immutability Anchor = React.cloneElement(child, { // @ts-ignore TODO - ref: anchorRef, + ref: combinedRef, onClick: child.props.onClick || onAnchorClick, 'aria-haspopup': true, 'aria-expanded': internalOpen, diff --git a/packages/react/src/experimental/Tabs/Tabs.examples.stories.tsx b/packages/react/src/experimental/Tabs/Tabs.examples.stories.tsx index ebe9a81a5aa..df2d7a518e4 100644 --- a/packages/react/src/experimental/Tabs/Tabs.examples.stories.tsx +++ b/packages/react/src/experimental/Tabs/Tabs.examples.stories.tsx @@ -17,7 +17,6 @@ const CustomTabList = (props: React.PropsWithChildren) => { return (
- {/* @ts-expect-error it needs a non nullable ref */} {props.children}
) diff --git a/packages/react/src/experimental/Tabs/Tabs.tsx b/packages/react/src/experimental/Tabs/Tabs.tsx index 69c891b5350..ad171ea4c41 100644 --- a/packages/react/src/experimental/Tabs/Tabs.tsx +++ b/packages/react/src/experimental/Tabs/Tabs.tsx @@ -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 @@ -114,13 +115,14 @@ function useTabList( 'aria-orientation': AriaAttributes['aria-orientation'] 'aria-label': AriaAttributes['aria-label'] 'aria-labelledby': AriaAttributes['aria-labelledby'] - ref: React.RefObject + ref: React.Ref role: 'tablist' } } { const {'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-orientation': ariaOrientation} = props - const ref = useProvidedRefOrCreate(props.ref) + const ref = useRef(null) + const mergedRef = useMergedRefs(ref, props.ref) const onKeyDown = (event: React.KeyboardEvent) => { const {current: tablist} = ref @@ -172,7 +174,7 @@ function useTabList( return { tabListProps: { - ref, + ref: combinedRef, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-orientation': ariaOrientation ?? 'horizontal', @@ -186,7 +188,6 @@ function TabList({children, ...rest}: TabListProps) { const {tabListProps} = useTabList(rest) return ( - // @ts-expect-error it needs a non nullable ref
{children}
diff --git a/packages/react/src/hooks/__tests__/useMergedRefs.test.tsx b/packages/react/src/hooks/__tests__/useMergedRefs.test.tsx new file mode 100644 index 00000000000..43ef7f8d706 --- /dev/null +++ b/packages/react/src/hooks/__tests__/useMergedRefs.test.tsx @@ -0,0 +1,153 @@ +import {render, renderHook} from '@testing-library/react' +import React, {forwardRef, type RefObject} from 'react' +import {describe, expect, it, vi} from 'vitest' +import {useMergedRefs} from '../useMergedRefs' + +type InputOrButtonRef = RefObject + +const Component = forwardRef(({asButton}, forwardedRef) => { + const ref: InputOrButtonRef = React.useRef(null) + + const mergedRef = useMergedRefs(forwardedRef, ref) + + return asButton ?