您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

index.tsx 5.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. 'use client'
  2. import React, { useCallback, useMemo, useState } from 'react'
  3. import {
  4. FloatingPortal,
  5. autoUpdate,
  6. flip,
  7. hide,
  8. offset,
  9. shift,
  10. size,
  11. useClick,
  12. useDismiss,
  13. useFloating,
  14. useFocus,
  15. useHover,
  16. useInteractions,
  17. useMergeRefs,
  18. useRole,
  19. } from '@floating-ui/react'
  20. import type { OffsetOptions, Placement } from '@floating-ui/react'
  21. import cn from '@/utils/classnames'
  22. export type PortalToFollowElemOptions = {
  23. /*
  24. * top, bottom, left, right
  25. * start, end. Default is middle
  26. * combine: top-start, top-end
  27. */
  28. placement?: Placement
  29. open?: boolean
  30. offset?: number | OffsetOptions
  31. onOpenChange?: (open: boolean) => void
  32. triggerPopupSameWidth?: boolean
  33. customContainer?: HTMLElement | null
  34. }
  35. export function usePortalToFollowElem({
  36. placement = 'bottom',
  37. open: controlledOpen,
  38. offset: offsetValue = 0,
  39. onOpenChange: setControlledOpen,
  40. triggerPopupSameWidth,
  41. customContainer = null,
  42. }: PortalToFollowElemOptions = {}) {
  43. const container = customContainer || document.getElementById('workflow-container') || document.body
  44. const [localOpen, setLocalOpen] = useState(false)
  45. const open = controlledOpen ?? localOpen
  46. const handleOpenChange = useCallback((newOpen: boolean) => {
  47. setLocalOpen(newOpen)
  48. setControlledOpen?.(newOpen)
  49. }, [setControlledOpen, setLocalOpen])
  50. const data = useFloating({
  51. placement,
  52. open,
  53. onOpenChange: handleOpenChange,
  54. whileElementsMounted: autoUpdate,
  55. middleware: [
  56. offset(offsetValue),
  57. flip({
  58. crossAxis: placement.includes('-'),
  59. fallbackAxisSideDirection: 'start',
  60. padding: 8,
  61. }),
  62. shift({
  63. padding: 8,
  64. boundary: container,
  65. altBoundary: true,
  66. }),
  67. hide({
  68. // hide when the reference element is not visible
  69. boundary: container,
  70. }),
  71. size({
  72. apply({ rects, elements }) {
  73. if (triggerPopupSameWidth)
  74. elements.floating.style.width = `${rects.reference.width}px`
  75. },
  76. }),
  77. ],
  78. })
  79. const context = data.context
  80. const hover = useHover(context, {
  81. move: false,
  82. enabled: open == null,
  83. })
  84. const focus = useFocus(context, {
  85. enabled: open == null,
  86. })
  87. const dismiss = useDismiss(context)
  88. const role = useRole(context, { role: 'tooltip' })
  89. const click = useClick(context)
  90. const interactionsArray = useMemo(() => {
  91. const result = [hover, focus, dismiss, role]
  92. if (!setControlledOpen)
  93. result.push(click)
  94. return result
  95. }, [setControlledOpen, hover, focus, dismiss, role, click])
  96. const interactions = useInteractions(interactionsArray)
  97. return React.useMemo(
  98. () => ({
  99. open,
  100. setOpen: handleOpenChange,
  101. ...interactions,
  102. ...data,
  103. }),
  104. [open, handleOpenChange, interactions, data],
  105. )
  106. }
  107. type ContextType = ReturnType<typeof usePortalToFollowElem> | null
  108. const PortalToFollowElemContext = React.createContext<ContextType>(null)
  109. export function usePortalToFollowElemContext() {
  110. const context = React.useContext(PortalToFollowElemContext)
  111. if (context == null)
  112. throw new Error('PortalToFollowElem components must be wrapped in <PortalToFollowElem />')
  113. return context
  114. }
  115. export function PortalToFollowElem({
  116. children,
  117. ...options
  118. }: { children: React.ReactNode } & PortalToFollowElemOptions) {
  119. // This can accept any props as options, e.g. `placement`,
  120. // or other positioning options.
  121. const tooltip = usePortalToFollowElem(options)
  122. return (
  123. <PortalToFollowElemContext.Provider value={tooltip}>
  124. {children}
  125. </PortalToFollowElemContext.Provider>
  126. )
  127. }
  128. export const PortalToFollowElemTrigger = (
  129. {
  130. ref: propRef,
  131. children,
  132. asChild = false,
  133. ...props
  134. }: React.HTMLProps<HTMLElement> & { ref?: React.RefObject<HTMLElement>, asChild?: boolean },
  135. ) => {
  136. const context = usePortalToFollowElemContext()
  137. const childrenRef = (children as any).props?.ref
  138. const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef])
  139. // `asChild` allows the user to pass any element as the anchor
  140. if (asChild && React.isValidElement(children)) {
  141. return React.cloneElement(
  142. children,
  143. context.getReferenceProps({
  144. ref,
  145. ...props,
  146. ...(children.props || {}),
  147. 'data-state': context.open ? 'open' : 'closed',
  148. } as React.HTMLProps<HTMLElement>),
  149. )
  150. }
  151. return (
  152. <div
  153. ref={ref}
  154. className={cn('inline-block', props.className)}
  155. // The user can style the trigger based on the state
  156. data-state={context.open ? 'open' : 'closed'}
  157. {...context.getReferenceProps(props)}
  158. >
  159. {children}
  160. </div>
  161. )
  162. }
  163. PortalToFollowElemTrigger.displayName = 'PortalToFollowElemTrigger'
  164. export const PortalToFollowElemContent = (
  165. {
  166. ref: propRef,
  167. style,
  168. ...props
  169. }: React.HTMLProps<HTMLDivElement> & {
  170. ref?: React.RefObject<HTMLDivElement>;
  171. },
  172. ) => {
  173. const context = usePortalToFollowElemContext()
  174. const ref = useMergeRefs([context.refs.setFloating, propRef])
  175. if (!context.open)
  176. return null
  177. const body = document.body
  178. return (
  179. <FloatingPortal root={body}>
  180. <div
  181. ref={ref}
  182. style={{
  183. ...context.floatingStyles,
  184. ...style,
  185. visibility: context.middlewareData.hide?.referenceHidden ? 'hidden' : 'visible',
  186. }}
  187. {...context.getFloatingProps(props)}
  188. />
  189. </FloatingPortal>
  190. )
  191. }
  192. PortalToFollowElemContent.displayName = 'PortalToFollowElemContent'