• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

clippingkk / web / #1178

21 Aug 2025 10:49AM UTC coverage: 0.523% (-0.02%) from 0.546%
#1178

push

web-flow
Merge c060881fd into 4eea96e38

30 of 448 branches covered (6.7%)

Branch coverage included in aggregate %.

12 of 7134 new or added lines in 368 files covered. (0.17%)

3 existing lines in 3 files now uncovered.

147 of 33423 relevant lines covered (0.44%)

8.69 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/components/RichTextEditor/plugins/FloatingMenu.tsx
1
import { computePosition } from '@floating-ui/dom'
×
2
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
×
3
import {
×
4
  $getSelection,
×
5
  $isRangeSelection,
×
6
  COMMAND_PRIORITY_NORMAL as NORMAL_PRIORITY,
×
7
  SELECTION_CHANGE_COMMAND as ON_SELECTION_CHANGE,
×
8
} from 'lexical'
×
9
// fork from: https://github.com/konstantinmuenster/lexical-floating-menu/blob/main/src/FloatingMenuPlugin.tsx
×
10
import { useCallback, useEffect, useRef, useState } from 'react'
×
11
import { createPortal } from 'react-dom'
×
12

×
13
import { usePointerInteractions } from '../hooks/usePointerInteractions'
×
14

×
15
const DEFAULT_DOM_ELEMENT = document.body
×
16

×
17
type FloatingMenuCoords = { x: number; y: number } | undefined
×
18

×
19
export type FloatingMenuComponentProps = {
×
20
  editor: ReturnType<typeof useLexicalComposerContext>[0]
×
21
  shouldShow: boolean
×
22
}
×
23

×
24
export type FloatingMenuPluginProps = {
×
25
  element?: HTMLElement | null
×
26
  MenuComponent: React.FC<FloatingMenuComponentProps>
×
27
}
×
28

×
29
function FloatingMenuPlugin({
×
30
  element,
×
31
  MenuComponent,
×
32
}: FloatingMenuPluginProps) {
×
33
  const ref = useRef<HTMLDivElement>(null)
×
34
  const [coords, setCoords] = useState<FloatingMenuCoords>(undefined)
×
35
  const show = coords !== undefined
×
36

×
37
  const [editor] = useLexicalComposerContext()
×
38
  const { isPointerDown, isPointerReleased } = usePointerInteractions()
×
39

×
40
  const calculatePosition = useCallback(() => {
×
41
    const domSelection = getSelection()
×
42
    const domRange =
×
43
      domSelection?.rangeCount !== 0 && domSelection?.getRangeAt(0)
×
44

×
45
    if (!domRange || !ref.current || isPointerDown) return setCoords(undefined)
×
46

×
47
    computePosition(domRange, ref.current, { placement: 'top' })
×
48
      .then((pos) => {
×
49
        setCoords({ x: pos.x, y: pos.y - 8 })
×
50
      })
×
51
      .catch(() => {
×
52
        setCoords(undefined)
×
53
      })
×
54
  }, [isPointerDown])
×
55

×
56
  const $handleSelectionChange = useCallback(() => {
×
57
    if (editor.isComposing()) return false
×
58

×
59
    if (editor.getRootElement() !== document.activeElement) {
×
60
      setCoords(undefined)
×
61
      return true
×
62
    }
×
63

×
64
    const selection = $getSelection()
×
65

×
66
    if ($isRangeSelection(selection) && !selection.anchor.is(selection.focus)) {
×
67
      calculatePosition()
×
68
    } else {
×
69
      setCoords(undefined)
×
70
    }
×
71

×
72
    return true
×
73
  }, [editor, calculatePosition])
×
74

×
75
  useEffect(() => {
×
76
    const unregisterCommand = editor.registerCommand(
×
77
      ON_SELECTION_CHANGE,
×
78
      $handleSelectionChange,
×
NEW
79
      NORMAL_PRIORITY
×
80
    )
×
81
    return unregisterCommand
×
82
  }, [editor, $handleSelectionChange])
×
83

×
84
  useEffect(() => {
×
85
    if (!show && isPointerReleased) {
×
86
      editor.getEditorState().read(() => {
×
87
        $handleSelectionChange()
×
88
      })
×
89
    }
×
90
    // Adding show to the dependency array causes an issue if
×
91
    // a range selection is dismissed by navigating via arrow keys.
×
92
    // eslint-disable-next-line react-hooks/exhaustive-deps
×
NEW
93
  }, [isPointerReleased, $handleSelectionChange, editor, show])
×
94

×
95
  if (!MenuComponent) return null
×
96

×
97
  return createPortal(
×
98
    <div
×
99
      ref={ref}
×
100
      aria-hidden={!show}
×
NEW
101
      className='absolute z-50 duration-100 transition-all top-0 left-0'
×
102
      style={{
×
103
        transform: `translate(${coords?.x}px, ${coords?.y}px)`,
×
104
        // top: coords?.y,
×
105
        // left: coords?.x,
×
106
        visibility: show ? 'visible' : 'hidden',
×
107
        opacity: show ? 1 : 0,
×
108
        height: 40,
×
109
        width: 280,
×
110
      }}
×
111
    >
×
112
      <MenuComponent editor={editor} shouldShow={show} />
×
113
    </div>,
×
NEW
114
    element ?? DEFAULT_DOM_ELEMENT
×
115
  )
×
116
}
×
117

×
118
export default FloatingMenuPlugin
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc