Spaces:
Build error
Build error
'use client' | |
import React from 'react' | |
import { | |
FloatingPortal, | |
autoUpdate, | |
flip, | |
offset, | |
shift, | |
useDismiss, | |
useFloating, | |
useFocus, | |
useHover, | |
useInteractions, | |
useMergeRefs, | |
useRole, | |
} from '@floating-ui/react' | |
import type { OffsetOptions, Placement } from '@floating-ui/react' | |
import cn from '@/utils/classnames' | |
export type PortalToFollowElemOptions = { | |
/* | |
* top, bottom, left, right | |
* start, end. Default is middle | |
* combine: top-start, top-end | |
*/ | |
placement?: Placement | |
open?: boolean | |
offset?: number | OffsetOptions | |
onOpenChange?: (open: boolean) => void | |
} | |
export function usePortalToFollowElem({ | |
placement = 'bottom', | |
open, | |
offset: offsetValue = 0, | |
onOpenChange: setControlledOpen, | |
}: PortalToFollowElemOptions = {}) { | |
const setOpen = setControlledOpen | |
const data = useFloating({ | |
placement, | |
open, | |
onOpenChange: setOpen, | |
whileElementsMounted: autoUpdate, | |
middleware: [ | |
offset(offsetValue), | |
flip({ | |
crossAxis: placement.includes('-'), | |
fallbackAxisSideDirection: 'start', | |
padding: 5, | |
}), | |
shift({ padding: 5 }), | |
], | |
}) | |
const context = data.context | |
const hover = useHover(context, { | |
move: false, | |
enabled: open == null, | |
}) | |
const focus = useFocus(context, { | |
enabled: open == null, | |
}) | |
const dismiss = useDismiss(context) | |
const role = useRole(context, { role: 'tooltip' }) | |
const interactions = useInteractions([hover, focus, dismiss, role]) | |
return React.useMemo( | |
() => ({ | |
open, | |
setOpen, | |
...interactions, | |
...data, | |
}), | |
[open, setOpen, interactions, data], | |
) | |
} | |
type ContextType = ReturnType<typeof usePortalToFollowElem> | null | |
const PortalToFollowElemContext = React.createContext<ContextType>(null) | |
export function usePortalToFollowElemContext() { | |
const context = React.useContext(PortalToFollowElemContext) | |
if (context == null) | |
throw new Error('PortalToFollowElem components must be wrapped in <PortalToFollowElem />') | |
return context | |
} | |
export function PortalToFollowElem({ | |
children, | |
...options | |
}: { children: React.ReactNode } & PortalToFollowElemOptions) { | |
// This can accept any props as options, e.g. `placement`, | |
// or other positioning options. | |
const tooltip = usePortalToFollowElem(options) | |
return ( | |
<PortalToFollowElemContext.Provider value={tooltip}> | |
{children} | |
</PortalToFollowElemContext.Provider> | |
) | |
} | |
export const PortalToFollowElemTrigger = React.forwardRef< | |
HTMLElement, | |
React.HTMLProps<HTMLElement> & { asChild?: boolean } | |
>(({ children, asChild = false, ...props }, propRef) => { | |
const context = usePortalToFollowElemContext() | |
const childrenRef = (children as any).ref | |
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]) | |
// `asChild` allows the user to pass any element as the anchor | |
if (asChild && React.isValidElement(children)) { | |
return React.cloneElement( | |
children, | |
context.getReferenceProps({ | |
ref, | |
...props, | |
...children.props, | |
'data-state': context.open ? 'open' : 'closed', | |
}), | |
) | |
} | |
return ( | |
<div | |
ref={ref} | |
className={cn('inline-block', props.className)} | |
// The user can style the trigger based on the state | |
data-state={context.open ? 'open' : 'closed'} | |
{...context.getReferenceProps(props)} | |
> | |
{children} | |
</div> | |
) | |
}) | |
PortalToFollowElemTrigger.displayName = 'PortalToFollowElemTrigger' | |
export const PortalToFollowElemContent = React.forwardRef< | |
HTMLDivElement, | |
React.HTMLProps<HTMLDivElement> | |
>(({ style, ...props }, propRef) => { | |
const context = usePortalToFollowElemContext() | |
const ref = useMergeRefs([context.refs.setFloating, propRef]) | |
if (!context.open) | |
return null | |
const body = document.body | |
return ( | |
<FloatingPortal root={body}> | |
<div | |
ref={ref} | |
style={{ | |
...context.floatingStyles, | |
...style, | |
}} | |
{...context.getFloatingProps(props)} | |
/> | |
</FloatingPortal> | |
) | |
}) | |
PortalToFollowElemContent.displayName = 'PortalToFollowElemContent' | |