Merge pull request #34311 from overleaf/mj-review-panel-with-tabs

[web] Move review panel header into tabs row when open

GitOrigin-RevId: e4594040f7a8ca67778fdd3e31cab909b3120adb
This commit is contained in:
Mathias Jakobsen
2026-06-08 08:06:31 +00:00
committed by Copybot
parent aed97f025c
commit 1a7b5e66f5
8 changed files with 88 additions and 2 deletions
@@ -58,6 +58,8 @@ const TabsContext = React.createContext<
setContextMenuTarget: React.Dispatch<
React.SetStateAction<TabsContextMenuTarget | null>
>
headerSlot: HTMLElement | null
setHeaderSlot: React.Dispatch<React.SetStateAction<HTMLElement | null>>
}
| undefined
>(undefined)
@@ -80,6 +82,8 @@ export const TabsProvider: FC<React.PropsWithChildren> = ({ children }) => {
const [contextMenuTarget, setContextMenuTarget] =
useState<TabsContextMenuTarget | null>(null)
const [headerSlot, setHeaderSlot] = useState<HTMLElement | null>(null)
const tabs = useMemo(() => {
if (!tabsEnabled) {
return []
@@ -297,6 +301,8 @@ export const TabsProvider: FC<React.PropsWithChildren> = ({ children }) => {
contextMenuTarget,
setContextMenuTarget,
closeToRight,
headerSlot,
setHeaderSlot,
}),
[
tabs,
@@ -308,6 +314,8 @@ export const TabsProvider: FC<React.PropsWithChildren> = ({ children }) => {
contextMenuTarget,
setContextMenuTarget,
closeToRight,
headerSlot,
setHeaderSlot,
]
)
@@ -0,0 +1,7 @@
import { FC, memo } from 'react'
const ReviewPanelHeaderBuffer: FC = () => {
return <div className="review-panel-header" aria-hidden />
}
export default memo(ReviewPanelHeaderBuffer)
@@ -0,0 +1,20 @@
import { FC } from 'react'
import { createPortal } from 'react-dom'
import ReviewPanelHeader from './review-panel-header'
import { useTabsContext } from '@/features/ide-react/context/tabs-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import useReviewPanelLayout from '../hooks/use-review-panel-layout'
const ReviewPanelTabsHeaderPortal: FC = () => {
const editorTabsEnabled = useFeatureFlag('editor-tabs')
const { headerSlot } = useTabsContext()
const { showHeader } = useReviewPanelLayout()
if (!editorTabsEnabled || !headerSlot || !showHeader) {
return null
}
return createPortal(<ReviewPanelHeader />, headerSlot)
}
export default ReviewPanelTabsHeaderPortal
@@ -11,6 +11,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac
import { FigureModal } from './figure-modal/figure-modal'
import { ReviewPanelProviders } from '@/features/review-panel/context/review-panel-providers'
import { ReviewPanelRoot } from '@/features/review-panel/components/review-panel-root'
import ReviewPanelTabsHeaderPortal from '@/features/review-panel/components/review-panel-tabs-header-portal'
import ReviewTooltipMenu from '@/features/review-panel/components/review-tooltip-menu'
import {
CodeMirrorStateContext,
@@ -104,6 +105,7 @@ function CodeMirrorEditorComponents({
<MathPreviewTooltip />
<EditorContextMenu />
{features.trackChangesVisible && <ReviewTooltipMenu />}
{features.trackChangesVisible && <ReviewPanelTabsHeaderPortal />}
{features.trackChangesVisible && <ReviewPanelRoot />}
{features.trackChangesVisible && <UpgradeTrackChangesModal />}
@@ -35,6 +35,7 @@ import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { useLayoutContext } from '@/shared/context/layout-context'
import ReviewPanelHeaderBuffer from '@/features/review-panel/components/review-panel-header-buffer'
const sourceEditorToolbarComponents = importOverleafModules(
'sourceEditorToolbarComponents'
@@ -159,6 +160,7 @@ const Toolbar = memo(function Toolbar() {
}, [buildOverflow, insideTable, resizeRef])
const showActions = !state.readOnly && !insideTable
const tabsVisible = useFeatureFlag('editor-tabs')
if (focusMode) {
return null
@@ -166,7 +168,8 @@ const Toolbar = memo(function Toolbar() {
return (
<>
{showReviewPanelHeader && <ReviewPanelHeader />}
{showReviewPanelHeader &&
(tabsVisible ? <ReviewPanelHeaderBuffer /> : <ReviewPanelHeader />)}
<div
id="ol-cm-toolbar-wrapper"
className={classNames('ol-cm-toolbar-wrapper', {
@@ -19,6 +19,7 @@ export const TabsContainer = () => {
moveTab,
makeTabPermanent,
setContextMenuTarget,
setHeaderSlot,
} = useTabsContext()
const { openEntity } = useFileTreeOpenContext()
const [hovered, setHovered] = useState<boolean>(false)
@@ -87,6 +88,7 @@ export const TabsContainer = () => {
return (
<div className="editor-tabs-container">
<div className="review-panel-header-slot" ref={setHeaderSlot} />
<div
className={classNames('editor-tabs-row', {
'editor-tabs-row-hovered': hovered,
@@ -1,6 +1,7 @@
:root {
--editor-tab-max-path-length: 250px;
--editor-tab-divider-color: var(--neutral-70);
--editor-tab-row-height: var(--review-panel-header-height, 36px);
}
@include theme('light') {
@@ -11,11 +12,15 @@
display: flex;
flex-direction: row;
align-items: center;
gap: var(--spacing-02);
flex: 0 0 auto;
min-height: var(--editor-tab-row-height);
background: var(--bg-secondary-themed);
justify-content: space-between;
.review-panel-header-slot {
display: contents;
}
.editor-tabs-labs-icon {
flex: 0 0 auto;
padding: 0 var(--spacing-02);
@@ -29,6 +34,7 @@
.editor-tabs-row {
display: flex;
flex-direction: row;
height: var(--editor-tab-row-height);
list-style: none;
padding: 0;
margin: 0;
@@ -47,6 +53,7 @@
--tab-bg-color: var(--bg-secondary-themed);
--tab-text-color: var(--content-secondary-themed);
display: flex;
border-right: 1px solid var(--editor-tab-divider-color);
border-left: 1px solid transparent;
padding: var(--spacing-01) var(--spacing-02) 0 var(--spacing-02);
@@ -18,6 +18,7 @@ import {
import { EditorView } from '@codemirror/view'
import { EditorState, Transaction } from '@codemirror/state'
import { tabsListener } from '@/features/source-editor/extensions/tabs-listener'
import ReviewPanelTabsHeaderPortal from '@/features/review-panel/components/review-panel-tabs-header-portal'
const DOC_IDS = {
main: 'doc-main-id',
@@ -247,6 +248,7 @@ describe('File Tabs', function () {
>
<FileSelectionDriver />
<TabsContainer />
<ReviewPanelTabsHeaderPortal />
<RemoteChangeButton />
</EditorProviders>
)
@@ -1158,6 +1160,41 @@ describe('File Tabs', function () {
})
})
describe('Review panel header', function () {
function toggleReviewPanel() {
cy.window().then(win => {
win.dispatchEvent(new win.CustomEvent('ui.toggle-review-panel'))
})
}
it('does not render the review panel header when the review panel is closed', function () {
cy.then(() => selectDoc(DOC_IDS.main))
cy.findByRole('heading', { name: 'Review' }).should('not.exist')
})
it('renders the review panel header inside the tabs container when the review panel is open', function () {
cy.then(() => selectDoc(DOC_IDS.main))
toggleReviewPanel()
cy.get('.editor-tabs-container').within(() => {
cy.findByRole('heading', { name: 'Review' }).should('exist')
})
})
it('removes the review panel header when the review panel is closed again', function () {
cy.then(() => selectDoc(DOC_IDS.main))
toggleReviewPanel()
cy.findByRole('heading', { name: 'Review' }).should('exist')
toggleReviewPanel()
cy.findByRole('heading', { name: 'Review' }).should('not.exist')
})
})
describe('SplitTestBadge', function () {
it('renders the labs badge icon in the tabs container', function () {
cy.window().then(win => {