@loke/ui
Version:
2 lines (1 loc) • 14.1 kB
JavaScript
import{composeEventHandlers}from"@loke/ui/compose-events";import{useComposedRefs}from"@loke/ui/compose-refs";import{createContextScope}from"@loke/ui/context";import{DismissableLayer}from"@loke/ui/dismissable-layer";import*as PopperPrimitive from"@loke/ui/popper";import{createPopperScope}from"@loke/ui/popper";import{Portal as PortalPrimitive}from"@loke/ui/portal";import{Presence}from"@loke/ui/presence";import{Primitive}from"@loke/ui/primitive";import{createSlottable}from"@loke/ui/slot";import{useControllableState}from"@loke/ui/use-controllable-state";import{useId}from"@loke/ui/use-id";import*as VisuallyHiddenPrimitive from"@loke/ui/visually-hidden";import{forwardRef,useCallback,useEffect,useMemo,useRef,useState}from"react";import{jsx,jsxs}from"react/jsx-runtime";var[createTooltipContext,createTooltipScope]=createContextScope("Tooltip",[createPopperScope]),usePopperScope=createPopperScope(),PROVIDER_NAME="TooltipProvider",DEFAULT_DELAY_DURATION=700,TOOLTIP_OPEN="tooltip.open",[TooltipProviderContextProvider,useTooltipProviderContext]=createTooltipContext(PROVIDER_NAME),TooltipProvider=(props)=>{let{__scopeTooltip,delayDuration=DEFAULT_DELAY_DURATION,skipDelayDuration=300,disableHoverableContent=!1,children}=props,isOpenDelayedRef=useRef(!0),isPointerInTransitRef=useRef(!1),skipDelayTimerRef=useRef(0);return useEffect(()=>{let skipDelayTimer=skipDelayTimerRef.current;return()=>window.clearTimeout(skipDelayTimer)},[]),jsx(TooltipProviderContextProvider,{delayDuration,disableHoverableContent,isOpenDelayedRef,isPointerInTransitRef,onClose:useCallback(()=>{window.clearTimeout(skipDelayTimerRef.current),skipDelayTimerRef.current=window.setTimeout(()=>isOpenDelayedRef.current=!0,skipDelayDuration)},[skipDelayDuration]),onOpen:useCallback(()=>{window.clearTimeout(skipDelayTimerRef.current),isOpenDelayedRef.current=!1},[]),onPointerInTransitChange:useCallback((inTransit)=>{isPointerInTransitRef.current=inTransit},[]),scope:__scopeTooltip,children})};TooltipProvider.displayName=PROVIDER_NAME;var TOOLTIP_NAME="Tooltip",[TooltipContextProvider,useTooltipContext]=createTooltipContext(TOOLTIP_NAME),Tooltip=(props)=>{let{__scopeTooltip,children,open:openProp,defaultOpen,onOpenChange,disableHoverableContent:disableHoverableContentProp,delayDuration:delayDurationProp}=props,providerContext=useTooltipProviderContext(TOOLTIP_NAME,props.__scopeTooltip),popperScope=usePopperScope(__scopeTooltip),[trigger,setTrigger]=useState(null),contentId=useId(),openTimerRef=useRef(0),disableHoverableContent=disableHoverableContentProp??providerContext.disableHoverableContent,delayDuration=delayDurationProp??providerContext.delayDuration,wasOpenDelayedRef=useRef(!1),[open,setOpen]=useControllableState({caller:TOOLTIP_NAME,defaultProp:defaultOpen??!1,onChange:(o)=>{if(o)providerContext.onOpen(),document.dispatchEvent(new CustomEvent(TOOLTIP_OPEN));else providerContext.onClose();onOpenChange?.(o)},prop:openProp}),stateAttribute=useMemo(()=>{if(!open)return"closed";return wasOpenDelayedRef.current?"delayed-open":"instant-open"},[open]),handleOpen=useCallback(()=>{window.clearTimeout(openTimerRef.current),openTimerRef.current=0,wasOpenDelayedRef.current=!1,setOpen(!0)},[setOpen]),handleClose=useCallback(()=>{window.clearTimeout(openTimerRef.current),openTimerRef.current=0,setOpen(!1)},[setOpen]),handleDelayedOpen=useCallback(()=>{window.clearTimeout(openTimerRef.current),openTimerRef.current=window.setTimeout(()=>{wasOpenDelayedRef.current=!0,setOpen(!0),openTimerRef.current=0},delayDuration)},[delayDuration,setOpen]);return useEffect(()=>{return()=>{if(openTimerRef.current)window.clearTimeout(openTimerRef.current),openTimerRef.current=0}},[]),jsx(PopperPrimitive.Root,{...popperScope,children:jsx(TooltipContextProvider,{contentId,disableHoverableContent,onClose:handleClose,onOpen:handleOpen,onTriggerChange:setTrigger,onTriggerEnter:useCallback(()=>{if(providerContext.isOpenDelayedRef.current)handleDelayedOpen();else handleOpen()},[providerContext.isOpenDelayedRef,handleDelayedOpen,handleOpen]),onTriggerLeave:useCallback(()=>{if(disableHoverableContent)handleClose();else window.clearTimeout(openTimerRef.current),openTimerRef.current=0},[handleClose,disableHoverableContent]),open,scope:__scopeTooltip,stateAttribute,trigger,children})})};Tooltip.displayName=TOOLTIP_NAME;var TRIGGER_NAME="TooltipTrigger",TooltipTrigger=forwardRef((props,forwardedRef)=>{let{__scopeTooltip,...triggerProps}=props,context=useTooltipContext(TRIGGER_NAME,__scopeTooltip),providerContext=useTooltipProviderContext(TRIGGER_NAME,__scopeTooltip),popperScope=usePopperScope(__scopeTooltip),ref=useRef(null),composedRefs=useComposedRefs(forwardedRef,ref,context.onTriggerChange),isPointerDownRef=useRef(!1),hasPointerMoveOpenedRef=useRef(!1),handlePointerUp=useCallback(()=>isPointerDownRef.current=!1,[]);return useEffect(()=>{return()=>document.removeEventListener("pointerup",handlePointerUp)},[handlePointerUp]),jsx(PopperPrimitive.Anchor,{asChild:!0,...popperScope,children:jsx(Primitive.button,{"aria-describedby":context.open?context.contentId:void 0,"data-state":context.stateAttribute,...triggerProps,onBlur:composeEventHandlers(props.onBlur,context.onClose),onClick:composeEventHandlers(props.onClick,context.onClose),onFocus:composeEventHandlers(props.onFocus,()=>{if(!isPointerDownRef.current)context.onOpen()}),onPointerDown:composeEventHandlers(props.onPointerDown,()=>{if(context.open)context.onClose();isPointerDownRef.current=!0,document.addEventListener("pointerup",handlePointerUp,{once:!0})}),onPointerLeave:composeEventHandlers(props.onPointerLeave,()=>{context.onTriggerLeave(),hasPointerMoveOpenedRef.current=!1}),onPointerMove:composeEventHandlers(props.onPointerMove,(event)=>{if(event.pointerType==="touch")return;if(!(hasPointerMoveOpenedRef.current||providerContext.isPointerInTransitRef.current))context.onTriggerEnter(),hasPointerMoveOpenedRef.current=!0}),ref:composedRefs})})});TooltipTrigger.displayName=TRIGGER_NAME;var PORTAL_NAME="TooltipPortal",[PortalProvider,usePortalContext]=createTooltipContext(PORTAL_NAME,{forceMount:void 0}),TooltipPortal=(props)=>{let{__scopeTooltip,forceMount,children,container}=props,context=useTooltipContext(PORTAL_NAME,__scopeTooltip);return jsx(PortalProvider,{forceMount,scope:__scopeTooltip,children:jsx(Presence,{present:forceMount||context.open,children:jsx(PortalPrimitive,{asChild:!0,container,children})})})};TooltipPortal.displayName=PORTAL_NAME;var CONTENT_NAME="TooltipContent",TooltipContent=forwardRef((props,forwardedRef)=>{let portalContext=usePortalContext(CONTENT_NAME,props.__scopeTooltip),{forceMount=portalContext.forceMount,side="top",...contentProps}=props,context=useTooltipContext(CONTENT_NAME,props.__scopeTooltip);return jsx(Presence,{present:forceMount||context.open,children:context.disableHoverableContent?jsx(TooltipContentImpl,{side,...contentProps,ref:forwardedRef}):jsx(TooltipContentHoverable,{side,...contentProps,ref:forwardedRef})})}),TooltipContentHoverable=forwardRef((props,forwardedRef)=>{let context=useTooltipContext(CONTENT_NAME,props.__scopeTooltip),providerContext=useTooltipProviderContext(CONTENT_NAME,props.__scopeTooltip),ref=useRef(null),composedRefs=useComposedRefs(forwardedRef,ref),[pointerGraceArea,setPointerGraceArea]=useState(null),{trigger,onClose}=context,content=ref.current,{onPointerInTransitChange}=providerContext,handleRemoveGraceArea=useCallback(()=>{setPointerGraceArea(null),onPointerInTransitChange(!1)},[onPointerInTransitChange]),handleCreateGraceArea=useCallback((event,hoverTarget)=>{let currentTarget=event.currentTarget,exitPoint={x:event.clientX,y:event.clientY},exitSide=getExitSideFromRect(exitPoint,currentTarget.getBoundingClientRect()),paddedExitPoints=getPaddedExitPoints(exitPoint,exitSide),hoverTargetPoints=getPointsFromRect(hoverTarget.getBoundingClientRect()),graceArea=getHull([...paddedExitPoints,...hoverTargetPoints]);setPointerGraceArea(graceArea),onPointerInTransitChange(!0)},[onPointerInTransitChange]);return useEffect(()=>{return()=>handleRemoveGraceArea()},[handleRemoveGraceArea]),useEffect(()=>{if(trigger&&content){let handleTriggerLeave=(event)=>handleCreateGraceArea(event,content),handleContentLeave=(event)=>handleCreateGraceArea(event,trigger);return trigger.addEventListener("pointerleave",handleTriggerLeave),content.addEventListener("pointerleave",handleContentLeave),()=>{trigger.removeEventListener("pointerleave",handleTriggerLeave),content.removeEventListener("pointerleave",handleContentLeave)}}},[trigger,content,handleCreateGraceArea,handleRemoveGraceArea]),useEffect(()=>{if(pointerGraceArea){let handleTrackPointerGrace=(event)=>{let target=event.target,pointerPosition={x:event.clientX,y:event.clientY},hasEnteredTarget=trigger?.contains(target)||content?.contains(target),isPointerOutsideGraceArea=!isPointInPolygon(pointerPosition,pointerGraceArea);if(hasEnteredTarget)handleRemoveGraceArea();else if(isPointerOutsideGraceArea)handleRemoveGraceArea(),onClose()};return document.addEventListener("pointermove",handleTrackPointerGrace),()=>document.removeEventListener("pointermove",handleTrackPointerGrace)}},[trigger,content,pointerGraceArea,onClose,handleRemoveGraceArea]),jsx(TooltipContentImpl,{...props,ref:composedRefs})}),[VisuallyHiddenContentContextProvider,useVisuallyHiddenContentContext]=createTooltipContext(TOOLTIP_NAME,{isInside:!1}),Slottable=createSlottable("TooltipContent"),TooltipContentImpl=forwardRef((props,forwardedRef)=>{let{__scopeTooltip,children,"aria-label":ariaLabel,onEscapeKeyDown,onPointerDownOutside,...contentProps}=props,context=useTooltipContext(CONTENT_NAME,__scopeTooltip),popperScope=usePopperScope(__scopeTooltip),{onClose}=context;return useEffect(()=>{return document.addEventListener(TOOLTIP_OPEN,onClose),()=>document.removeEventListener(TOOLTIP_OPEN,onClose)},[onClose]),useEffect(()=>{if(context.trigger){let handleScroll=(event)=>{if(event.target?.contains(context.trigger))onClose()};return window.addEventListener("scroll",handleScroll,{capture:!0}),()=>window.removeEventListener("scroll",handleScroll,{capture:!0})}},[context.trigger,onClose]),jsx(DismissableLayer,{asChild:!0,disableOutsidePointerEvents:!1,onDismiss:onClose,onEscapeKeyDown,onFocusOutside:(event)=>event.preventDefault(),onPointerDownOutside,children:jsxs(PopperPrimitive.Content,{"data-state":context.stateAttribute,...popperScope,...contentProps,ref:forwardedRef,style:{...contentProps.style,...{"--loke-tooltip-content-available-height":"var(--loke-popper-available-height)","--loke-tooltip-content-available-width":"var(--loke-popper-available-width)","--loke-tooltip-content-transform-origin":"var(--loke-popper-transform-origin)","--loke-tooltip-trigger-height":"var(--loke-popper-anchor-height)","--loke-tooltip-trigger-width":"var(--loke-popper-anchor-width)"}},children:[jsx(Slottable,{children}),jsx(VisuallyHiddenContentContextProvider,{isInside:!0,scope:__scopeTooltip,children:jsx(VisuallyHiddenPrimitive.Root,{id:context.contentId,role:"tooltip",children:ariaLabel||children})})]})})});TooltipContent.displayName=CONTENT_NAME;var ARROW_NAME="TooltipArrow",TooltipArrow=forwardRef((props,forwardedRef)=>{let{__scopeTooltip,...arrowProps}=props,popperScope=usePopperScope(__scopeTooltip);return useVisuallyHiddenContentContext(ARROW_NAME,__scopeTooltip).isInside?null:jsx(PopperPrimitive.Arrow,{...popperScope,...arrowProps,ref:forwardedRef})});TooltipArrow.displayName=ARROW_NAME;function getExitSideFromRect(point,rect){let top=Math.abs(rect.top-point.y),bottom=Math.abs(rect.bottom-point.y),right=Math.abs(rect.right-point.x),left=Math.abs(rect.left-point.x);switch(Math.min(top,bottom,right,left)){case left:return"left";case right:return"right";case top:return"top";case bottom:return"bottom";default:throw Error("unreachable")}}function getPaddedExitPoints(exitPoint,exitSide,padding=5){let paddedExitPoints=[];switch(exitSide){case"top":paddedExitPoints.push({x:exitPoint.x-padding,y:exitPoint.y+padding},{x:exitPoint.x+padding,y:exitPoint.y+padding});break;case"bottom":paddedExitPoints.push({x:exitPoint.x-padding,y:exitPoint.y-padding},{x:exitPoint.x+padding,y:exitPoint.y-padding});break;case"left":paddedExitPoints.push({x:exitPoint.x+padding,y:exitPoint.y-padding},{x:exitPoint.x+padding,y:exitPoint.y+padding});break;case"right":paddedExitPoints.push({x:exitPoint.x-padding,y:exitPoint.y-padding},{x:exitPoint.x-padding,y:exitPoint.y+padding});break}return paddedExitPoints}function getPointsFromRect(rect){let{top,right,bottom,left}=rect;return[{x:left,y:top},{x:right,y:top},{x:right,y:bottom},{x:left,y:bottom}]}function isPointInPolygon(point,polygon){let{x,y}=point,inside=!1;for(let i=0,j=polygon.length-1;i<polygon.length;j=i++){let ii=polygon[i],jj=polygon[j];if(ii==null||jj==null)continue;let{x:xi,y:yi}=ii,xj=jj.x,yj=jj.y;if(yi>y!==yj>y&&x<(xj-xi)*(y-yi)/(yj-yi)+xi)inside=!inside}return inside}function getHull(points){let newPoints=points.slice();return newPoints.sort((a,b)=>{if(a.x<b.x)return-1;if(a.x>b.x)return 1;if(a.y<b.y)return-1;if(a.y>b.y)return 1;return 0}),getHullPresorted(newPoints)}function getHullPresorted(points){if(points.length<=1)return points.slice();let upperHull=[];for(let p of points){while(upperHull.length>=2){let q=upperHull.at(-1),r=upperHull.at(-2);if(q==null||r==null)break;if((q.x-r.x)*(p.y-r.y)>=(q.y-r.y)*(p.x-r.x))upperHull.pop();else break}upperHull.push(p)}upperHull.pop();let lowerHull=[];for(let p of points.toReversed()){while(lowerHull.length>=2){let q=lowerHull.at(-1),r=lowerHull.at(-2);if(q==null||r==null)break;if((q.x-r.x)*(p.y-r.y)>=(q.y-r.y)*(p.x-r.x))lowerHull.pop();else break}lowerHull.push(p)}if(lowerHull.pop(),upperHull.length===1&&lowerHull.length===1&&upperHull[0]?.x===lowerHull[0]?.x&&upperHull[0]?.y===lowerHull[0]?.y)return upperHull;return upperHull.concat(lowerHull)}var Provider=TooltipProvider,Root3=Tooltip,Trigger=TooltipTrigger,Portal=TooltipPortal,Content2=TooltipContent,Arrow2=TooltipArrow;export{createTooltipScope,Trigger,TooltipTrigger,TooltipProvider,TooltipPortal,TooltipContent,TooltipArrow,Tooltip,Root3 as Root,Provider,Portal,Content2 as Content,Arrow2 as Arrow};