[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
@@ -487,6 +487,7 @@ const _ProjectController = {
'command-palette', 'command-palette',
'overleaf-library', 'overleaf-library',
'compile-timeout-cta', 'compile-timeout-cta',
'focus-mode',
'editor-upgrade-button-relocation', 'editor-upgrade-button-relocation',
'markdown-visual', 'markdown-visual',
].filter(Boolean) ].filter(Boolean)
@@ -645,6 +645,7 @@
"event_type": "", "event_type": "",
"example_project": "", "example_project": "",
"existing_plan_active_until_term_end": "", "existing_plan_active_until_term_end": "",
"exit_focus_mode": "",
"expand": "", "expand": "",
"experiment_enabled_refresh_page": "", "experiment_enabled_refresh_page": "",
"experiment_full_check_back_soon": "", "experiment_full_check_back_soon": "",
@@ -705,6 +706,7 @@
"fix_errors": "", "fix_errors": "",
"fixed_width": "", "fixed_width": "",
"fixed_width_wrap_text": "", "fixed_width_wrap_text": "",
"focus_mode": "",
"fold_line": "", "fold_line": "",
"folder_location": "", "folder_location": "",
"folder_name": "", "folder_name": "",
@@ -12,10 +12,12 @@ export default function RailPanel({
isReviewPanelOpen, isReviewPanelOpen,
isHistoryView, isHistoryView,
railTabs, railTabs,
focusMode,
}: { }: {
isReviewPanelOpen: boolean isReviewPanelOpen: boolean
isHistoryView: boolean isHistoryView: boolean
railTabs: RailElement[] railTabs: RailElement[]
focusMode: boolean
}) { }) {
const { selectedTab, panelRef, handlePaneExpand, handlePaneCollapse } = const { selectedTab, panelRef, handlePaneExpand, handlePaneCollapse } =
useRailContext() useRailContext()
@@ -35,7 +37,7 @@ export default function RailPanel({
return ( return (
<Panel <Panel
id={`ide-redesign-sidebar-panel-${isHistoryView ? 'file-tree' : selectedTab}`} id={`ide-redesign-sidebar-panel-${isHistoryView ? 'file-tree' : selectedTab}`}
className={classNames({ hidden: isReviewPanelOpen })} className={classNames({ hidden: isReviewPanelOpen || focusMode })}
order={1} order={1}
defaultSize={selectedTab === 'workbench' ? 20 : 15} defaultSize={selectedTab === 'workbench' ? 20 : 15}
minSize={5} minSize={5}
@@ -6,15 +6,17 @@ import { useTranslation } from 'react-i18next'
export default function RailResizeHandle({ export default function RailResizeHandle({
isReviewPanelOpen, isReviewPanelOpen,
focusMode,
}: { }: {
isReviewPanelOpen: boolean isReviewPanelOpen: boolean
focusMode: boolean
}) { }) {
const { isOpen, setIsOpen, togglePane, setResizing } = useRailContext() const { isOpen, setIsOpen, togglePane, setResizing } = useRailContext()
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<HorizontalResizeHandle <HorizontalResizeHandle
className={classNames({ hidden: isReviewPanelOpen })} className={classNames({ hidden: isReviewPanelOpen || focusMode })}
resizable resizable
hitAreaMargins={{ coarse: 0, fine: 0 }} hitAreaMargins={{ coarse: 0, fine: 0 }}
onDoubleClick={togglePane} onDoubleClick={togglePane}
@@ -73,7 +73,7 @@ export const RailLayout = () => {
const gitBridgeEnabled = getMeta('ol-gitBridgeEnabled') const gitBridgeEnabled = getMeta('ol-gitBridgeEnabled')
const { isOverleaf } = getMeta('ol-ExposedSettings') const { isOverleaf } = getMeta('ol-ExposedSettings')
const { view, setLeftMenuShown } = useLayoutContext() const { view, setLeftMenuShown, focusMode } = useLayoutContext()
const { markMessagesAsRead } = useChatContext() const { markMessagesAsRead } = useChatContext()
@@ -271,7 +271,9 @@ export const RailLayout = () => {
But it should be identified as a navigation landmark. But it should be identified as a navigation landmark.
Therefore, we nest them: the parent <nav> is the landmark, and its child gets the "role="tablist"". */} Therefore, we nest them: the parent <nav> is the landmark, and its child gets the "role="tablist"". */}
<nav <nav
className={classNames('ide-rail', { hidden: isHistoryView })} className={classNames('ide-rail', {
hidden: isHistoryView || focusMode,
})}
aria-label={t('sidebar')} aria-label={t('sidebar')}
> >
<Nav activeKey={selectedTab} className="ide-rail-tabs-nav"> <Nav activeKey={selectedTab} className="ide-rail-tabs-nav">
@@ -306,17 +308,20 @@ export const RailLayout = () => {
</nav> </nav>
</Nav> </Nav>
</nav> </nav>
{moduleRailPopovers {!focusMode &&
.filter(shouldIncludeElement) moduleRailPopovers
.map(({ key, Component, ref }) => ( .filter(shouldIncludeElement)
<Component key={key} ref={ref} /> .map(({ key, Component, ref }) => <Component key={key} ref={ref} />)}
))}
<RailPanel <RailPanel
isReviewPanelOpen={isReviewPanelOpen} isReviewPanelOpen={isReviewPanelOpen}
isHistoryView={isHistoryView} isHistoryView={isHistoryView}
railTabs={railTabs} railTabs={railTabs}
focusMode={focusMode}
/>
<RailResizeHandle
isReviewPanelOpen={isReviewPanelOpen}
focusMode={focusMode}
/> />
<RailResizeHandle isReviewPanelOpen={isReviewPanelOpen} />
<RailModals /> <RailModals />
</TabContainer> </TabContainer>
) )
@@ -1,6 +1,7 @@
import { import {
DropdownItem, DropdownItem,
DropdownHeader, DropdownHeader,
DropdownDivider,
} from '@/shared/components/dropdown/dropdown-menu' } from '@/shared/components/dropdown/dropdown-menu'
import { import {
IdeLayout, IdeLayout,
@@ -14,8 +15,16 @@ import OLSpinner from '@/shared/components/ol/ol-spinner'
import { isMac } from '@/shared/utils/os' import { isMac } from '@/shared/utils/os'
import { Shortcut } from '@/shared/components/shortcut' import { Shortcut } from '@/shared/components/shortcut'
import classNames from 'classnames' 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 = ({ const getActiveLayoutOption = ({
pdfLayout, pdfLayout,
@@ -45,7 +54,6 @@ const getActiveLayoutOption = ({
if (pdfLayout === 'sideBySide') { if (pdfLayout === 'sideBySide') {
return 'sideBySide' return 'sideBySide'
} }
return null return null
} }
@@ -93,12 +101,14 @@ const shortcuts: Record<LayoutOption, string[] | null> = isMac
pdfOnly: ['⌃', '⌘', '→'], pdfOnly: ['⌃', '⌘', '→'],
sideBySide: ['⌃', '⌘', '↓'], sideBySide: ['⌃', '⌘', '↓'],
detachedPdf: ['⌃', '⌘', '↑'], detachedPdf: ['⌃', '⌘', '↑'],
focusMode: ['⌘', '⇧', 'M'],
} }
: { : {
editorOnly: null, editorOnly: null,
pdfOnly: null, pdfOnly: null,
sideBySide: null, sideBySide: null,
detachedPdf: null, detachedPdf: null,
focusMode: ['⌃', '⇧', 'M'],
} }
export default function ChangeLayoutOptions() { export default function ChangeLayoutOptions() {
@@ -109,9 +119,13 @@ export default function ChangeLayoutOptions() {
pdfLayout, pdfLayout,
handleChangeLayout, handleChangeLayout,
handleDetach, handleDetach,
focusMode,
setFocusMode,
} = useLayoutContext() } = useLayoutContext()
const { t } = useTranslation() const { t } = useTranslation()
const { sendEvent } = useEditorAnalytics()
const focusModeEnabled = useFeatureFlag('focus-mode')
const detachable = 'BroadcastChannel' in window const detachable = 'BroadcastChannel' in window
@@ -168,6 +182,30 @@ export default function ChangeLayoutOptions() {
> >
{t('open_pdf_in_separate_tab')} {t('open_pdf_in_separate_tab')}
</LayoutDropdownItem> </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 getMeta from '@/utils/meta'
import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context' import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
import { useFeatureFlag } from '@/shared/context/split-test-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 [publishModalModules] = importOverleafModules('publishModal')
const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton const SubmitProjectButton = publishModalModules?.import.NewPublishToolbarButton
export const Toolbar = () => { export const Toolbar = () => {
const { view, restoreView } = useLayoutContext() const { view, restoreView, focusMode, setFocusMode, pdfLayout, setView } =
useLayoutContext()
const { cobranding, isRestrictedTokenMember } = useEditorContext() const { cobranding, isRestrictedTokenMember } = useEditorContext()
const { permissionsLevel } = useIdeReactContext() const { permissionsLevel } = useIdeReactContext()
const showUpgradePrompt = getMeta('ol-showUpgradePrompt') const showUpgradePrompt = getMeta('ol-showUpgradePrompt')
@@ -38,6 +42,72 @@ export const Toolbar = () => {
restoreView() restoreView()
}, [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') { if (view === 'history') {
return ( return (
<nav className="ide-redesign-toolbar" aria-label={t('project_actions')}> <nav className="ide-redesign-toolbar" aria-label={t('project_actions')}>
@@ -1,3 +1,4 @@
import { useLayoutContext } from '@/shared/context/layout-context'
import { memo } from 'react' import { memo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import OLButtonToolbar from '@/shared/components/ol/ol-button-toolbar' import OLButtonToolbar from '@/shared/components/ol/ol-button-toolbar'
@@ -34,6 +35,7 @@ function PdfPreviewHybridToolbar() {
} }
function PdfPreviewHybridToolbarInner() { function PdfPreviewHybridToolbarInner() {
const { focusMode } = useLayoutContext()
return ( return (
<> <>
<div className="toolbar-pdf-left"> <div className="toolbar-pdf-left">
@@ -43,7 +45,7 @@ function PdfPreviewHybridToolbarInner() {
</div> </div>
<div className="toolbar-pdf-right"> <div className="toolbar-pdf-right">
<div className="toolbar-pdf-controls" id="toolbar-pdf-controls" /> <div className="toolbar-pdf-controls" id="toolbar-pdf-controls" />
<SwitchToEditorButton /> {!focusMode && <SwitchToEditorButton />}
<DetachedSynctexControl /> <DetachedSynctexControl />
{/* TODO: should we have code check? */} {/* TODO: should we have code check? */}
</div> </div>
@@ -4,10 +4,12 @@ import { memo } from 'react'
import ReviewPanel from './review-panel' import ReviewPanel from './review-panel'
import ReviewModeSwitcher from './review-mode-switcher' import ReviewModeSwitcher from './review-mode-switcher'
import useReviewPanelLayout from '../hooks/use-review-panel-layout' import useReviewPanelLayout from '../hooks/use-review-panel-layout'
import { useLayoutContext } from '@/shared/context/layout-context'
function ReviewPanelContainer() { function ReviewPanelContainer() {
const view = useCodeMirrorViewContext() const view = useCodeMirrorViewContext()
const { showPanel, mini } = useReviewPanelLayout() const { showPanel, mini } = useReviewPanelLayout()
const { focusMode } = useLayoutContext()
if (!view) { if (!view) {
return null return null
@@ -15,7 +17,7 @@ function ReviewPanelContainer() {
return ReactDOM.createPortal( return ReactDOM.createPortal(
<> <>
<ReviewModeSwitcher /> {!focusMode && <ReviewModeSwitcher />}
{showPanel && <ReviewPanel mini={mini} />} {showPanel && <ReviewPanel mini={mini} />}
</>, </>,
view.scrollDOM view.scrollDOM
@@ -1,6 +1,7 @@
import { useRangesContext } from '../context/ranges-context' import { useRangesContext } from '../context/ranges-context'
import { useThreadsContext } from '@/features/review-panel/context/threads-context' import { useThreadsContext } from '@/features/review-panel/context/threads-context'
import { hasActiveRange } from '@/features/review-panel/utils/has-active-range' import { hasActiveRange } from '@/features/review-panel/utils/has-active-range'
import { useLayoutContext } from '@/shared/context/layout-context'
import { useRailContext } from '@/features/ide-react/context/rail-context' import { useRailContext } from '@/features/ide-react/context/rail-context'
import { useCallback } from 'react' import { useCallback } from 'react'
@@ -19,6 +20,7 @@ export default function useReviewPanelLayout(): {
openTab: openRailTab, openTab: openRailTab,
setIsOpen: setRailIsOpen, setIsOpen: setRailIsOpen,
} = useRailContext() } = useRailContext()
const { focusMode } = useLayoutContext()
const reviewPanelOpen = selectedRailTab === 'review-panel' && railIsOpen const reviewPanelOpen = selectedRailTab === 'review-panel' && railIsOpen
@@ -31,7 +33,7 @@ export default function useReviewPanelLayout(): {
}, [setRailIsOpen]) }, [setRailIsOpen])
const hasCommentOrChange = hasActiveRange(ranges, threads) const hasCommentOrChange = hasActiveRange(ranges, threads)
const showPanel = reviewPanelOpen || !!hasCommentOrChange const showPanel = focusMode ? false : reviewPanelOpen || !!hasCommentOrChange
const mini = !reviewPanelOpen const mini = !reviewPanelOpen
const showHeader = showPanel && !mini const showHeader = showPanel && !mini
@@ -34,6 +34,7 @@ import classNames from 'classnames'
import { useUserSettingsContext } from '@/shared/context/user-settings-context' import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useFeatureFlag } from '@/shared/context/split-test-context' import { useFeatureFlag } from '@/shared/context/split-test-context'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro' import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { useLayoutContext } from '@/shared/context/layout-context'
const sourceEditorToolbarComponents = importOverleafModules( const sourceEditorToolbarComponents = importOverleafModules(
'sourceEditorToolbarComponents' 'sourceEditorToolbarComponents'
@@ -62,6 +63,7 @@ const Toolbar = memo(function Toolbar() {
userSettings: { breadcrumbs }, userSettings: { breadcrumbs },
} = useUserSettingsContext() } = useUserSettingsContext()
const visualPreviewEnabled = useFeatureFlag('visual-preview') const visualPreviewEnabled = useFeatureFlag('visual-preview')
const { focusMode } = useLayoutContext()
const [overflowed, setOverflowed] = useState(false) const [overflowed, setOverflowed] = useState(false)
@@ -158,6 +160,10 @@ const Toolbar = memo(function Toolbar() {
const showActions = !state.readOnly && !insideTable const showActions = !state.readOnly && !insideTable
if (focusMode) {
return null
}
return ( return (
<> <>
{showReviewPanelHeader && <ReviewPanelHeader />} {showReviewPanelHeader && <ReviewPanelHeader />}
@@ -24,6 +24,7 @@ import { useRailContext } from '@/features/ide-react/context/rail-context'
import usePersistedState from '@/shared/hooks/use-persisted-state' import usePersistedState from '@/shared/hooks/use-persisted-state'
import { repositionAllTooltips } from '@/features/source-editor/extensions/tooltips-reposition' import { repositionAllTooltips } from '@/features/source-editor/extensions/tooltips-reposition'
import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics' import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
import { useFeatureFlag } from './split-test-context'
export type IdeLayout = 'sideBySide' | 'flat' export type IdeLayout = 'sideBySide' | 'flat'
export type IdeView = 'editor' | 'file' | 'pdf' | 'history' export type IdeView = 'editor' | 'file' | 'pdf' | 'history'
@@ -38,6 +39,7 @@ export type LayoutContextOwnStates = {
pdfLayout: IdeLayout pdfLayout: IdeLayout
projectSearchIsOpen: boolean projectSearchIsOpen: boolean
openFile: BinaryFile | null openFile: BinaryFile | null
focusMode: boolean
} }
export type LayoutContextValue = LayoutContextOwnStates & { export type LayoutContextValue = LayoutContextOwnStates & {
@@ -66,6 +68,7 @@ export type LayoutContextValue = LayoutContextOwnStates & {
restoreView: () => void restoreView: () => void
handleChangeLayout: (newLayout: IdeLayout, newView?: IdeView) => void handleChangeLayout: (newLayout: IdeLayout, newView?: IdeView) => void
handleDetach: () => void handleDetach: () => void
setFocusMode: Dispatch<SetStateAction<LayoutContextValue['focusMode']>>
} }
const debugPdfDetach = getMeta('ol-debugPdfDetach') const debugPdfDetach = getMeta('ol-debugPdfDetach')
@@ -154,6 +157,37 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
// whether the project search is open // whether the project search is open
const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(false) const [projectSearchIsOpen, setProjectSearchIsOpen] = useState(false)
// whether to display the editor and preview side-by-side or full-width ("flat")
const [pdfLayout, setPdfLayout] = useState<IdeLayout>('sideBySide')
const [persistedFocusMode, setPersistedFocusMode] =
usePersistedState<boolean>('ui.focus-mode', false)
const focusModeEnabled = useFeatureFlag('focus-mode')
const focusMode = focusModeEnabled && persistedFocusMode
const setFocusMode = useCallback(
(value: SetStateAction<boolean>) => {
if (focusModeEnabled) {
const newValue = typeof value === 'function' ? value(focusMode) : value
setPersistedFocusMode(newValue)
sendEvent('project-layout-change', {
layout: pdfLayout,
view: view ?? undefined,
focusMode: newValue,
})
}
},
[
focusMode,
focusModeEnabled,
pdfLayout,
sendEvent,
setPersistedFocusMode,
view,
]
)
useEventListener( useEventListener(
'ui.toggle-left-menu', 'ui.toggle-left-menu',
useCallback( useCallback(
@@ -172,14 +206,21 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
}, [setReviewPanelOpen]) }, [setReviewPanelOpen])
) )
// TODO ide-redesign-cleanup: remove this listener as we have an equivalent in rail-context
useEventListener( useEventListener(
'keydown', 'ui.toggle-focus-mode',
useCallback((event: KeyboardEvent) => { useCallback(() => {
setFocusMode(mode => !mode)
}, [setFocusMode])
)
// Global keyboard shortcut handlers
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// Cmd/Ctrl+Shift+F for full project search
if ( if (
(isMac ? event.metaKey : event.ctrlKey) && (isMac ? event.metaKey : event.ctrlKey) &&
event.shiftKey && event.shiftKey &&
event.code === 'KeyF' event.key === 'F'
) { ) {
event.preventDefault() event.preventDefault()
sendSearchEvent('search-open', { sendSearchEvent('search-open', {
@@ -188,11 +229,25 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
}) })
setProjectSearchIsOpen(true) setProjectSearchIsOpen(true)
} }
}, []) // Cmd/Ctrl+Shift+M for focus mode
) if (
focusModeEnabled &&
(isMac ? event.metaKey : event.ctrlKey) &&
event.shiftKey &&
event.key === 'M'
) {
event.preventDefault()
setFocusMode(mode => !mode)
}
}
// whether to display the editor and preview side-by-side or full-width ("flat") // Use capture phase to ensure we get the event even if something else stops propagation
const [pdfLayout, setPdfLayout] = useState<IdeLayout>('sideBySide') window.addEventListener('keydown', handleKeyDown, true)
return () => {
window.removeEventListener('keydown', handleKeyDown, true)
}
}, [focusModeEnabled, setFocusMode, setProjectSearchIsOpen])
// whether stylesheet on theme is loading // whether stylesheet on theme is loading
const [loadingStyleSheet, setLoadingStyleSheet] = useState(false) const [loadingStyleSheet, setLoadingStyleSheet] = useState(false)
@@ -282,9 +337,10 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
sendEvent('project-layout-change', { sendEvent('project-layout-change', {
layout: newLayout, layout: newLayout,
view: newView, view: newView,
focusMode,
}) })
}, },
[changeLayout, handleReattach, sendEvent] [changeLayout, focusMode, handleReattach, sendEvent]
) )
useEventListener( useEventListener(
@@ -353,6 +409,8 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
restoreView, restoreView,
handleChangeLayout, handleChangeLayout,
handleDetach, handleDetach,
focusMode,
setFocusMode,
}), }),
[ [
reattach, reattach,
@@ -382,6 +440,8 @@ export const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
restoreView, restoreView,
handleChangeLayout, handleChangeLayout,
handleDetach, handleDetach,
focusMode,
setFocusMode,
] ]
) )
+2
View File
@@ -849,6 +849,7 @@
"examples_to_help_you_learn": "Examples to help you learn how to use powerful LaTeX packages and techniques.", "examples_to_help_you_learn": "Examples to help you learn how to use powerful LaTeX packages and techniques.",
"exclusive_access_with_labs": "Exclusive access to early-stage experiments", "exclusive_access_with_labs": "Exclusive access to early-stage experiments",
"existing_plan_active_until_term_end": "Your existing plan and its features will remain active until the end of the current billing period.", "existing_plan_active_until_term_end": "Your existing plan and its features will remain active until the end of the current billing period.",
"exit_focus_mode": "Exit focus mode",
"expand": "Expand", "expand": "Expand",
"experiment_enabled_refresh_page": "Experiment is enabled! You need to refresh the page to see the changes.", "experiment_enabled_refresh_page": "Experiment is enabled! You need to refresh the page to see the changes.",
"experiment_full_check_back_soon": "Sorry, this experiment is full. Spaces may become available, so check back soon.", "experiment_full_check_back_soon": "Sorry, this experiment is full. Spaces may become available, so check back soon.",
@@ -927,6 +928,7 @@
"fixed_width_wrap_text": "Fixed width, wrap text", "fixed_width_wrap_text": "Fixed width, wrap text",
"flexible_plans_for_everyone": "Flexible plans for everyone—from individual students and researchers, to large businesses and universities.", "flexible_plans_for_everyone": "Flexible plans for everyone—from individual students and researchers, to large businesses and universities.",
"flexible_plans_for_everyone_from_individual_students_and_researchers_to_large_businesses_and_universities": "Flexible plans for everyone, from individual students and researchers to large businesses and universities.", "flexible_plans_for_everyone_from_individual_students_and_researchers_to_large_businesses_and_universities": "Flexible plans for everyone, from individual students and researchers to large businesses and universities.",
"focus_mode": "Focus mode",
"fold_line": "Fold line", "fold_line": "Fold line",
"folder_location": "Folder location", "folder_location": "Folder location",
"folder_name": "Folder name", "folder_name": "Folder name",
@@ -98,6 +98,8 @@ const createInitialValue = () =>
restoreView: cy.stub(), restoreView: cy.stub(),
handleChangeLayout: cy.stub(), handleChangeLayout: cy.stub(),
handleDetach: cy.stub(), handleDetach: cy.stub(),
focusMode: false,
setFocusMode: cy.stub(),
}) satisfies LayoutContextValue }) satisfies LayoutContextValue
const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => { const LayoutProvider: FC<React.PropsWithChildren> = ({ children }) => {
@@ -577,6 +577,8 @@ const makeLayoutProvider = (
restoreView, restoreView,
handleChangeLayout, handleChangeLayout,
handleDetach, handleDetach,
focusMode: layout.focusMode ?? false,
setFocusMode: layout.setFocusMode ?? (() => {}),
}), }),
[ [
reattach, reattach,