[web] Add a focus mode to the editor (#32245)

* Basic focus mode implementation

* Exit button

* Toggle between editor pdf mode

* Keep the rail and file tree mounted for editor tabs to work properly

* Hide Switch to editor button in focus mode

* Show shortcuts in layout menu

* Gate the feature behind a focus-mode feature flag

* Add split test badges in the focus mode dropdown item and top bar

* Add analytics tracking for focusMode changes

* Cleanup leftover IdeView value

* Allow passing focusMode value in layout test provider

* Remove redundant listener for focus mode shortcut

* Update focus mode and full project search listeners to use the event key

* Post-rebase cleanup

---------

Co-authored-by: Anna Fields <acfields11@gmail.com>
GitOrigin-RevId: 207f8d136fa6662558c474cca4a867a9842191e7
This commit is contained in:
Alexandre Bourdin
2026-06-08 08:06:05 +00:00
committed by Copybot
co-authored by Anna Fields
parent 757735b075
commit c8a3bdd13f
15 changed files with 223 additions and 25 deletions
@@ -1,6 +1,7 @@
import {
DropdownItem,
DropdownHeader,
DropdownDivider,
} from '@/shared/components/dropdown/dropdown-menu'
import {
IdeLayout,
@@ -14,8 +15,16 @@ import OLSpinner from '@/shared/components/ol/ol-spinner'
import { isMac } from '@/shared/utils/os'
import { Shortcut } from '@/shared/components/shortcut'
import classNames from 'classnames'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import SplitTestBadge from '@/shared/components/split-test-badge'
type LayoutOption = 'sideBySide' | 'editorOnly' | 'pdfOnly' | 'detachedPdf'
type LayoutOption =
| 'sideBySide'
| 'editorOnly'
| 'pdfOnly'
| 'detachedPdf'
| 'focusMode'
const getActiveLayoutOption = ({
pdfLayout,
@@ -45,7 +54,6 @@ const getActiveLayoutOption = ({
if (pdfLayout === 'sideBySide') {
return 'sideBySide'
}
return null
}
@@ -93,12 +101,14 @@ const shortcuts: Record<LayoutOption, string[] | null> = isMac
pdfOnly: ['⌃', '⌘', '→'],
sideBySide: ['⌃', '⌘', '↓'],
detachedPdf: ['⌃', '⌘', '↑'],
focusMode: ['⌘', '⇧', 'M'],
}
: {
editorOnly: null,
pdfOnly: null,
sideBySide: null,
detachedPdf: null,
focusMode: ['⌃', '⇧', 'M'],
}
export default function ChangeLayoutOptions() {
@@ -109,9 +119,13 @@ export default function ChangeLayoutOptions() {
pdfLayout,
handleChangeLayout,
handleDetach,
focusMode,
setFocusMode,
} = useLayoutContext()
const { t } = useTranslation()
const { sendEvent } = useEditorAnalytics()
const focusModeEnabled = useFeatureFlag('focus-mode')
const detachable = 'BroadcastChannel' in window
@@ -168,6 +182,30 @@ export default function ChangeLayoutOptions() {
>
{t('open_pdf_in_separate_tab')}
</LayoutDropdownItem>
{focusModeEnabled && (
<>
<DropdownDivider />
<LayoutDropdownItem
onClick={() => {
setFocusMode(!focusMode)
sendEvent('focus-mode-toggle', { focusMode: !focusMode })
}}
active={focusMode}
leadingIcon="self_improvement"
trailingIcon={
<span className="d-flex align-items-center gap-2">
<SplitTestBadge
splitTestName="focus-mode"
displayOnVariants={['enabled']}
/>
{shortcuts.focusMode && <Shortcut keys={shortcuts.focusMode} />}
</span>
}
>
{t('focus_mode')}
</LayoutDropdownItem>
</>
)}
</>
)
}
@@ -16,12 +16,16 @@ import UpgradeButton from './upgrade-button'
import getMeta from '@/utils/meta'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import OLIconButton from '@/shared/components/ol/ol-icon-button'
import OLTooltip from '@/shared/components/ol/ol-tooltip'
import SplitTestBadge from '@/shared/components/split-test-badge'
const [publishModalModules] = importOverleafModules('publishModal')
const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
export const Toolbar = () => {
const { view, restoreView } = useLayoutContext()
const { view, restoreView, focusMode, setFocusMode, pdfLayout, setView } =
useLayoutContext()
const { cobranding, isRestrictedTokenMember } = useEditorContext()
const { permissionsLevel } = useIdeReactContext()
const showUpgradePrompt = getMeta('ol-showUpgradePrompt')
@@ -38,6 +42,72 @@ export const Toolbar = () => {
restoreView()
}, [restoreView])
const handleExitFocusMode = useCallback(() => {
setFocusMode(false)
eventTracking.sendMB('focus-mode-exit')
}, [setFocusMode])
const handleSwitchView = useCallback(() => {
const newView = view === 'pdf' ? 'editor' : 'pdf'
setView(newView)
eventTracking.sendMB('focus-mode-switch-view', { view: newView })
}, [view, setView])
if (focusMode) {
const showViewSwitcher = pdfLayout === 'flat'
const switchTooltip =
view === 'pdf' ? t('switch_to_editor') : t('switch_to_pdf')
const switchIcon = view === 'pdf' ? 'edit' : 'picture_as_pdf'
return (
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
<div className="ide-redesign-toolbar-menu">
<ToolbarLogos cobranding={cobranding} />
</div>
<ToolbarProjectTitle />
<div className="ide-redesign-toolbar-actions">
<div className="ide-redesign-toolbar-button-container">
<SplitTestBadge
splitTestName="focus-mode"
displayOnVariants={['enabled']}
/>
</div>
{showViewSwitcher && (
<div className="ide-redesign-toolbar-button-container">
<OLTooltip
id="tooltip-switch-view"
description={switchTooltip}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<OLIconButton
icon={switchIcon}
className="ide-redesign-toolbar-button-subdued ide-redesign-toolbar-button-icon"
onClick={handleSwitchView}
accessibilityLabel={switchTooltip}
/>
</OLTooltip>
</div>
)}
<ChangeLayoutButton />
<div className="ide-redesign-toolbar-button-container">
<OLTooltip
id="tooltip-exit-focus-mode"
description={t('exit_focus_mode')}
overlayProps={{ delay: 0, placement: 'bottom' }}
>
<OLIconButton
icon="close"
className="ide-redesign-toolbar-button-subdued ide-redesign-toolbar-button-icon"
onClick={handleExitFocusMode}
accessibilityLabel={t('exit_focus_mode')}
/>
</OLTooltip>
</div>
</div>
</nav>
)
}
if (view === 'history') {
return (
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>