Merge pull request #34681 from overleaf/mfb-34187-move-editor-switch-to-right-hand

Introduce shared ToolbarLayout component and move editor switch to right in bibtex visual toolbar

GitOrigin-RevId: 5f61bf8144dc68dfb4adce8a03d43c2dd0de3369
This commit is contained in:
Maria Florencia Besteiro Gonzalez
2026-09-17 11:18:57 +00:00
committed by Copybot
parent f30c23cacc
commit b93d180ee9
5 changed files with 238 additions and 77 deletions
@@ -76,12 +76,10 @@ function CodeMirrorEditor() {
return (
<CodeMirrorStateContext.Provider value={state}>
<CodeMirrorViewContext.Provider value={viewRef.current}>
<CodeMirrorEditorComponents hidden={VisualEditor != null} />
{VisualEditor && (
<Suspense fallback={null}>
<VisualEditor />
</Suspense>
)}
<CodeMirrorEditorComponents
hidden={VisualEditor != null}
VisualEditor={VisualEditor}
/>
</CodeMirrorViewContext.Provider>
</CodeMirrorStateContext.Provider>
)
@@ -89,10 +87,12 @@ function CodeMirrorEditor() {
type CodeMirrorEditorComponentsProps = {
hidden: boolean
VisualEditor: ElementType | null
}
function CodeMirrorEditorComponents({
hidden = false,
VisualEditor,
}: CodeMirrorEditorComponentsProps) {
useToolbarMenuBarEditorCommands()
const { features } = useProjectContext()
@@ -132,6 +132,11 @@ function CodeMirrorEditorComponents({
<Component key={path} />
)
)}
{VisualEditor && (
<Suspense fallback={null}>
<VisualEditor />
</Suspense>
)}
</ReviewPanelProviders>
)
}
@@ -17,9 +17,6 @@ import { ToolbarOverflow } from './toolbar/overflow'
import useDropdown from '../../../shared/hooks/use-dropdown'
import { getPanel } from '@codemirror/view'
import { createToolbarPanel } from '../extensions/toolbar/toolbar-panel'
import EditorSwitch from './editor-switch'
import ReviewModeSwitcher from '@/features/review-panel/components/review-mode-switcher'
import SwitchToPDFButton from './switch-to-pdf-button'
import { DetacherSynctexControl } from '../../pdf-preview/components/detach-synctex-control'
import DetachCompileButtonWrapper from '../../pdf-preview/components/detach-compile-button-wrapper'
import { isVisual } from '../extensions/visual/visual'
@@ -27,31 +24,21 @@ import { language } from '@codemirror/language'
import { minimumListDepthForSelection } from '../utils/tree-operations/ancestors'
import { debugConsole } from '@/utils/debugging'
import { useTranslation } from 'react-i18next'
import { ToggleSearchButton } from '@/features/source-editor/components/toolbar/toggle-search-button'
import ReviewPanelHeader from '@/features/review-panel/components/review-panel-header'
import useReviewPanelLayout from '@/features/review-panel/hooks/use-review-panel-layout'
import Breadcrumbs from '@/features/source-editor/extensions/breadcrumbs'
import classNames from 'classnames'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useProjectContext } from '@/shared/context/project-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'
import { useAreTabsEnabled } from '@/features/ide-react/hooks/use-are-tabs-enabled'
const sourceEditorToolbarStartButtons = importOverleafModules(
'sourceEditorToolbarStartButtons'
) as { import: { default: ElementType }; path: string }[]
import { ToolbarLayout } from './toolbar/toolbar-layout'
const sourceEditorToolbarComponents = importOverleafModules(
'sourceEditorToolbarComponents'
) as { import: { default: ElementType }; path: string }[]
const sourceEditorToolbarEndButtons = importOverleafModules(
'sourceEditorToolbarEndButtons'
) as { import: { default: ElementType }; path: string }[]
export const CodeMirrorToolbar = () => {
const view = useCodeMirrorViewContext()
const panel = getPanel(view, createToolbarPanel)
@@ -70,9 +57,6 @@ const Toolbar = memo(function Toolbar() {
const {
userSettings: { breadcrumbs },
} = useUserSettingsContext()
const visualPreviewEnabled = useFeatureFlag('visual-preview')
const isToolbarMigration = useFeatureFlag('writefull-toolbar-migration')
const { features } = useProjectContext()
const { focusMode } = useLayoutContext()
const [overflowed, setOverflowed] = useState(false)
@@ -202,21 +186,19 @@ const Toolbar = memo(function Toolbar() {
className="ol-cm-toolbar toolbar-editor"
ref={handleToolbar}
>
{showActions &&
sourceEditorToolbarStartButtons.map(
({ import: { default: Component }, path }) => (
<Component key={path} />
)
<ToolbarLayout.Left
canEdit={!state.readOnly}
showActions={showActions}
>
{showActions && (
<ToolbarItems
state={state}
languageName={languageName}
visual={visual}
listDepth={listDepth}
/>
)}
{showActions && (
<ToolbarItems
state={state}
languageName={languageName}
visual={visual}
listDepth={listDepth}
/>
)}
</ToolbarLayout.Left>
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-stretch">
{showActions && (
<ToolbarOverflow
@@ -235,26 +217,10 @@ const Toolbar = memo(function Toolbar() {
</ToolbarOverflow>
)}
</div>
<div className="ol-cm-toolbar-button-group ol-cm-toolbar-end">
{!visualPreviewEnabled && <EditorSwitch />}
{/* trackChangesVisible controls provider/UI availability; trackChanges
(checked inside the switcher) controls the actual feature entitlement.
Users with trackChangesVisible:true but trackChanges:false see the
switcher and get an upgrade modal when clicking "Reviewing". */}
{isToolbarMigration && features.trackChangesVisible && (
<ReviewModeSwitcher />
)}
{sourceEditorToolbarEndButtons.map(
({ import: { default: Component }, path }) => (
<Component key={path} />
)
)}
<ToggleSearchButton state={state} />
<SwitchToPDFButton />
<ToolbarLayout.Right canSearchInFile={true}>
<DetacherSynctexControl />
<DetachCompileButtonWrapper />
</div>
</ToolbarLayout.Right>
</div>
{sourceEditorToolbarComponents.map(
({ import: { default: Component }, path }) => (
@@ -2,7 +2,6 @@ import { ComponentType, FC, memo } from 'react'
import { EditorState } from '@codemirror/state'
import { useEditorContext } from '../../../../shared/context/editor-context'
import { ToolbarButton } from './toolbar-button'
import { redo, undo } from '@codemirror/commands'
import * as commands from '../../extensions/toolbar/commands'
import { SectionHeadingDropdown } from './section-heading-dropdown'
import getMeta from '../../../../utils/meta'
@@ -57,27 +56,6 @@ export const ToolbarItems: FC<{
return (
<>
{showGroup('group-history') && (
<div
className="ol-cm-toolbar-button-group"
aria-label={t('toolbar_undo_redo_actions')}
>
<ToolbarButton
id="toolbar-undo"
label={t('toolbar_undo')}
command={undo}
icon="undo"
shortcut={isMac ? '⌘Z' : 'Ctrl+Z'}
/>
<ToolbarButton
id="toolbar-redo"
label={t('toolbar_redo')}
command={redo}
icon="redo"
shortcut={isMac ? '⇧⌘Z' : 'Ctrl+Y'}
/>
</div>
)}
{sourceEditorToolbarButtonGroups.map(
({ import: { default: Component, overflowGroupId }, path }) =>
showGroup(overflowGroupId) && <Component key={path} />
@@ -0,0 +1,134 @@
import { ReactNode, ElementType } from 'react'
import '../../../../../stylesheets/components/editor-toolbar.scss'
import { ToolbarButton } from './toolbar-button'
import { undo, redo } from '@codemirror/commands'
import { useTranslation } from 'react-i18next'
import { isMac } from '@/shared/utils/os'
import { useCodeMirrorStateContext } from '@/features/source-editor/components/codemirror-context'
import { useProjectContext } from '@/shared/context/project-context'
import EditorSwitch from '@/features/source-editor/components/editor-switch'
import ReviewModeSwitcher from '@/features/review-panel/components/review-mode-switcher'
import { ToggleSearchButton } from './toggle-search-button'
import SwitchToPDFButton from '../switch-to-pdf-button'
import importOverleafModules from '../../../../../macros/import-overleaf-module.macro'
import { useFeatureFlag } from '@/shared/context/split-test-context'
type ToolbarLayoutProps = {
children: ReactNode
className?: string
}
type ToolbarLayoutLeftProps = {
children?: ReactNode
canEdit?: boolean
showActions?: boolean
}
type ToolbarLayoutRightProps = {
canSearchInFile?: boolean
canUseWritefull?: boolean
children?: ReactNode
}
const sourceEditorToolbarEndButtons = importOverleafModules(
'sourceEditorToolbarEndButtons'
) as { import: { default: ElementType }; path: string }[]
const sourceEditorToolbarStartButtons = importOverleafModules(
'sourceEditorToolbarStartButtons'
) as { import: { default: ElementType }; path: string }[]
function Left({ children, canEdit, showActions }: ToolbarLayoutLeftProps) {
const { t } = useTranslation()
return (
<div className="ol-toolbar-layout-left">
{showActions &&
sourceEditorToolbarStartButtons.map(
({ import: { default: Component }, path }) => <Component key={path} />
)}
{canEdit && (
<div
className="ol-editor-toolbar-button-group"
aria-label={t('toolbar_undo_redo_actions')}
>
<ToolbarButton
id="toolbar-undo"
label={t('toolbar_undo')}
command={undo}
icon="undo"
shortcut={isMac ? '⌘Z' : 'Ctrl+Z'}
/>
<ToolbarButton
id="toolbar-redo"
label={t('toolbar_redo')}
command={redo}
icon="redo"
shortcut={isMac ? '⇧⌘Z' : 'Ctrl+Y'}
/>
</div>
)}
{children}
</div>
)
}
function Right({
canSearchInFile,
canUseWritefull = true,
children,
}: ToolbarLayoutRightProps) {
const state = useCodeMirrorStateContext()
const visualPreviewEnabled = useFeatureFlag('visual-preview')
const isToolbarMigration = useFeatureFlag('writefull-toolbar-migration')
const { features } = useProjectContext()
return (
<div className="ol-toolbar-layout-right">
{!visualPreviewEnabled && <EditorSwitch />}
{/* trackChangesVisible controls provider/UI availability; trackChanges
(checked inside the switcher) controls the actual feature entitlement.
Users with trackChangesVisible:true but trackChanges:false see the
switcher and get an upgrade modal when clicking "Reviewing". */}
{isToolbarMigration &&
canUseWritefull !== false &&
features.trackChangesVisible && <ReviewModeSwitcher />}
{!isToolbarMigration && (
<div
style={{
display: 'flex',
visibility: canUseWritefull ? 'visible' : 'hidden',
}}
>
{sourceEditorToolbarEndButtons.map(
({ import: { default: Component }, path }) => (
<Component key={path} />
)
)}
</div>
)}
<div
style={{
display: 'flex',
visibility: canSearchInFile ? 'visible' : 'hidden',
}}
>
<ToggleSearchButton state={state} />
</div>
<SwitchToPDFButton />
{children}
</div>
)
}
type ToolbarLayoutCompoundComponent = ((
props: ToolbarLayoutProps
) => JSX.Element) & {
Left: typeof Left
Right: typeof Right
}
export const ToolbarLayout = (({ children, className }: ToolbarLayoutProps) => (
<div className={className}>{children}</div>
)) as ToolbarLayoutCompoundComponent
ToolbarLayout.Left = Left
ToolbarLayout.Right = Right
@@ -0,0 +1,78 @@
.ol-editor-toolbar-button-group {
display: flex;
align-items: center;
white-space: nowrap;
flex-wrap: nowrap;
padding: 0 4px;
margin: 4px 0;
line-height: 1;
&:not(:first-child) {
border-left: 1px solid rgb(125 125 125 / 30%);
}
.ol-cm-toolbar-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0 1px;
background-color: transparent;
border: none;
border-radius: var(--border-radius-base);
line-height: 1;
width: 24px;
height: 24px;
overflow: hidden;
color: inherit;
&:hover,
&:focus,
&:active,
&.active {
background-color: rgb(125 125 125 / 10%);
color: inherit;
box-shadow: none;
&[aria-disabled='true'] {
opacity: 0.2;
}
}
&.active,
&:active {
background-color: rgb(125 125 125 / 20%);
}
&[aria-disabled='true'] {
opacity: 0.2;
cursor: not-allowed;
}
}
}
.ol-cm-toolbar-wrapper-needs-border
.ol-editor-toolbar-button-group:first-child {
border-left: 1px solid rgb(125 125 125 / 30%);
}
.ol-toolbar-layout-right {
margin-left: auto;
display: flex;
align-items: center;
.ol-cm-toolbar-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0 1px;
width: 24px;
height: 24px;
}
}
.ol-toolbar-layout-left {
display: flex;
align-items: center;
}