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

index.tsx 4.7KB

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