Merge pull request #35173 from overleaf/worktree-mg-ai-actions-position

Move floating menu and writefull AI actions to bottom of selection

GitOrigin-RevId: 2d94c2fc9513b8fd42b1904433382eca898d8d99
This commit is contained in:
Malik Glossop
2026-09-17 11:23:36 +00:00
committed by Copybot
parent 63968ad775
commit 414027160d
@@ -9,6 +9,7 @@ import React, {
import ReactDOM from 'react-dom'
import classNames from 'classnames'
import { getTooltip } from '@codemirror/view'
import { EditorState } from '@codemirror/state'
import importOverleafModules from '../../../macros/import-overleaf-module.macro'
import {
useCodeMirrorStateContext,
@@ -23,6 +24,15 @@ import TrackedChangesActions from './components/tracked-changes-actions'
const TOOLTIP_SHOW_DELAY = 120
// The document position on the visual bottom line of the selection, so the menu
// anchors to the last line regardless of drag direction. If the selection ends
// at column 0 of a multi-line selection, the bottom line is the previous one.
function selectionBottomLinePos(state: EditorState) {
const { from, to } = state.selection.main
const lineAtTo = state.doc.lineAt(to)
return lineAtTo.from === to && to > from ? to - 1 : to
}
// Each default-exports a self-gating component.
const editorFloatingMenuActions = importOverleafModules(
'editorFloatingMenuActions'
@@ -87,8 +97,10 @@ const EditorFloatingMenuContent = memo(function EditorFloatingMenuContent() {
view.requestMeasure({
key: 'editor-floating-menu-position',
read(view) {
const cursorCoords = view.coordsAtPos(view.state.selection.main.head)
if (!cursorCoords) {
const lineCoords = view.coordsAtPos(
selectionBottomLinePos(view.state)
)
if (!lineCoords) {
return
}
@@ -96,21 +108,19 @@ const EditorFloatingMenuContent = memo(function EditorFloatingMenuContent() {
menuRef.current?.getBoundingClientRect().height ?? 0
const scrollDomRect = view.scrollDOM.getBoundingClientRect()
const contentDomRect = view.contentDOM.getBoundingClientRect()
const cursorCenterY = (cursorCoords.top + cursorCoords.bottom) / 2
const lineCenterY = (lineCoords.top + lineCoords.bottom) / 2
if (
// Cursor scrolls out of view at the top
cursorCoords.top < scrollDomRect.top ||
// Cursor scrolls out of view at the bottom
cursorCoords.top > scrollDomRect.bottom
) {
return { visibility: 'hidden' as const }
}
// Centre on the line, but keep the menu pinned inside the editor
// viewport so it never scrolls out of view: when the anchored line
// leaves the top or bottom edge the menu rides that edge instead.
const top = Math.min(
Math.max(lineCenterY - menuHeight / 2, scrollDomRect.top),
scrollDomRect.bottom - menuHeight
)
return {
position: 'fixed' as const,
// Align centrally
top: cursorCenterY - menuHeight / 2,
top,
right: window.innerWidth - contentDomRect.left,
}
},
@@ -119,15 +129,9 @@ const EditorFloatingMenuContent = memo(function EditorFloatingMenuContent() {
write(res) {
const el = menuRef.current
if (!el || !res) return
// Only toggle visibility when off-screen
if (res.visibility === 'hidden') {
el.style.visibility = 'hidden'
} else {
el.style.visibility = ''
el.style.position = res.position
el.style.top = `${res.top}px`
el.style.right = `${res.right}px`
}
el.style.position = res.position
el.style.top = `${res.top}px`
el.style.right = `${res.right}px`
},
})
}