Merge pull request #29899 from overleaf/mj-dark-mode-file-flash

[web] Avoid background color flash when switching files

GitOrigin-RevId: e5d2fbb631fd54d195b9cb51b2a9db584d205138
This commit is contained in:
Mathias Jakobsen
2025-11-28 09:05:18 +00:00
committed by Copybot
parent ff5b469b20
commit 7c1a225be4
15 changed files with 145 additions and 85 deletions
@@ -1308,49 +1308,49 @@ const defaultUserValues = () => ({
}) })
const THEME_LIST = [ const THEME_LIST = [
'cobalt', { name: 'cobalt', dark: true },
'dracula', { name: 'dracula', dark: true },
'eclipse', { name: 'eclipse', dark: false },
'monokai', { name: 'monokai', dark: true },
'overleaf', { name: 'overleaf', dark: false },
'overleaf_dark', { name: 'overleaf_dark', dark: true },
'textmate', { name: 'textmate', dark: false },
] ]
const LEGACY_THEME_LIST = [ const LEGACY_THEME_LIST = [
'ambiance', { name: 'ambiance', dark: true },
'chaos', { name: 'chaos', dark: true },
'chrome', { name: 'chrome', dark: false },
'clouds', { name: 'clouds', dark: false },
'clouds_midnight', { name: 'clouds_midnight', dark: true },
'crimson_editor', { name: 'crimson_editor', dark: false },
'dawn', { name: 'dawn', dark: false },
'dreamweaver', { name: 'dreamweaver', dark: false },
'github', { name: 'github', dark: false },
'gob', { name: 'gob', dark: true },
'gruvbox', { name: 'gruvbox', dark: true },
'idle_fingers', { name: 'idle_fingers', dark: true },
'iplastic', { name: 'iplastic', dark: false },
'katzenmilch', { name: 'katzenmilch', dark: false },
'kr_theme', { name: 'kr_theme', dark: true },
'kuroir', { name: 'kuroir', dark: false },
'merbivore', { name: 'merbivore', dark: true },
'merbivore_soft', { name: 'merbivore_soft', dark: true },
'mono_industrial', { name: 'mono_industrial', dark: true },
'nord_dark', { name: 'nord_dark', dark: true },
'pastel_on_dark', { name: 'pastel_on_dark', dark: true },
'solarized_dark', { name: 'solarized_dark', dark: true },
'solarized_light', { name: 'solarized_light', dark: false },
'sqlserver', { name: 'sqlserver', dark: false },
'terminal', { name: 'terminal', dark: true },
'tomorrow', { name: 'tomorrow', dark: false },
'tomorrow_night', { name: 'tomorrow_night', dark: true },
'tomorrow_night_blue', { name: 'tomorrow_night_blue', dark: true },
'tomorrow_night_bright', { name: 'tomorrow_night_bright', dark: true },
'tomorrow_night_eighties', { name: 'tomorrow_night_eighties', dark: true },
'twilight', { name: 'twilight', dark: true },
'vibrant_ink', { name: 'vibrant_ink', dark: true },
'xcode', { name: 'xcode', dark: false },
] ]
const ProjectController = { const ProjectController = {
@@ -14,8 +14,8 @@ export default function SettingsEditorTheme() {
const options = useMemo(() => { const options = useMemo(() => {
const editorThemeOptions: Array<Option> = const editorThemeOptions: Array<Option> =
editorThemes?.map(theme => ({ editorThemes?.map(theme => ({
value: theme, value: theme.name,
label: theme.replace(/_/g, ' '), label: theme.name.replace(/_/g, ' '),
})) ?? [] })) ?? []
const dividerOption: Option = { const dividerOption: Option = {
@@ -26,8 +26,8 @@ export default function SettingsEditorTheme() {
const legacyEditorThemeOptions: Array<Option> = const legacyEditorThemeOptions: Array<Option> =
legacyEditorThemes?.map(theme => ({ legacyEditorThemes?.map(theme => ({
value: theme, value: theme.name,
label: theme.replace(/_/g, ' ') + ' (Legacy)', label: theme.name.replace(/_/g, ' ') + ' (Legacy)',
})) ?? [] })) ?? []
return [...editorThemeOptions, dividerOption, ...legacyEditorThemeOptions] return [...editorThemeOptions, dividerOption, ...legacyEditorThemeOptions]
@@ -0,0 +1,25 @@
import { FC, useMemo } from 'react'
import LoadingSpinner from '@/shared/components/loading-spinner'
import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
import getMeta from '@/utils/meta'
import classNames from 'classnames'
export const EditorLoadingPane: FC = () => {
const { editorTheme } = useProjectSettingsContext()
const isDark = useMemo(() => {
const themes = getMeta('ol-editorThemes') || []
const legacyThemes = getMeta('ol-legacyEditorThemes') || []
const selectedTheme =
themes.find(theme => theme.name === editorTheme) ||
legacyThemes.find(theme => theme.name === editorTheme)
return selectedTheme?.dark ?? false
}, [editorTheme])
return (
<div
className={classNames('loading-panel', { 'loading-panel-dark': isDark })}
>
<LoadingSpinner />
</div>
)
}
@@ -4,7 +4,7 @@ import SourceEditor from '@/features/source-editor/components/source-editor'
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context' import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import classNames from 'classnames' import classNames from 'classnames'
import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane' import { EditorLoadingPane } from '@/features/ide-react/components/editor/editor-loading-pane'
import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner' import { FullSizeLoadingSpinner } from '@/shared/components/loading-spinner'
import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle' import { VerticalResizeHandle } from '@/features/ide-react/components/resize/vertical-resize-handle'
import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context' import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
@@ -37,7 +37,7 @@ export const EditorPane: FC = () => {
className="ide-react-editor-panel" className="ide-react-editor-panel"
> >
<SourceEditor /> <SourceEditor />
{isLoading && <LoadingPane />} {isLoading && <EditorLoadingPane />}
</Panel> </Panel>
{showSymbolPalette && ( {showSymbolPalette && (
@@ -1,10 +0,0 @@
import { FC } from 'react'
import LoadingSpinner from '@/shared/components/loading-spinner'
export const LoadingPane: FC = () => {
return (
<div className="loading-panel">
<LoadingSpinner />
</div>
)
}
@@ -1,4 +1,4 @@
import { LoadingPane } from '@/features/ide-react/components/editor/loading-pane' import { EditorLoadingPane } from '@/features/ide-react/components/editor/editor-loading-pane'
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context' import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context' import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
import classNames from 'classnames' import classNames from 'classnames'
@@ -40,7 +40,7 @@ export const Editor = () => {
className="ide-redesign-editor-panel" className="ide-redesign-editor-panel"
> >
<SourceEditor /> <SourceEditor />
{isLoading && <LoadingPane />} {isLoading && <EditorLoadingPane />}
</Panel> </Panel>
{showSymbolPalette && ( {showSymbolPalette && (
<> <>
@@ -14,8 +14,8 @@ export default function EditorThemeSetting() {
const options = useMemo(() => { const options = useMemo(() => {
const editorThemeOptions: Array<Option> = const editorThemeOptions: Array<Option> =
editorThemes?.map(theme => ({ editorThemes?.map(theme => ({
value: theme, value: theme.name,
label: theme.replace(/_/g, ' '), label: theme.name.replace(/_/g, ' '),
})) ?? [] })) ?? []
const dividerOption: Option = { const dividerOption: Option = {
@@ -26,8 +26,8 @@ export default function EditorThemeSetting() {
const legacyEditorThemeOptions: Array<Option> = const legacyEditorThemeOptions: Array<Option> =
legacyEditorThemes?.map(theme => ({ legacyEditorThemes?.map(theme => ({
value: theme, value: theme.name,
label: theme.replace(/_/g, ' ') + ' (Legacy)', label: theme.name.replace(/_/g, ' ') + ' (Legacy)',
})) ?? [] })) ?? []
return [...editorThemeOptions, dividerOption, ...legacyEditorThemeOptions] return [...editorThemeOptions, dividerOption, ...legacyEditorThemeOptions]
@@ -287,7 +287,8 @@ const loadSelectedTheme = async (editorTheme: string) => {
const themes = getMeta('ol-editorThemes') || [] const themes = getMeta('ol-editorThemes') || []
const legacyThemes = getMeta('ol-legacyEditorThemes') || [] const legacyThemes = getMeta('ol-legacyEditorThemes') || []
const themeExists = const themeExists =
themes.includes(editorTheme) || legacyThemes.includes(editorTheme) themes.some(theme => theme.name === editorTheme) ||
legacyThemes.some(theme => theme.name === editorTheme)
if (!themeExists) { if (!themeExists) {
editorTheme = 'textmate' // fallback to default if the theme is not found editorTheme = 'textmate' // fallback to default if the theme is not found
} }
+2 -2
View File
@@ -117,7 +117,7 @@ export interface Meta {
'ol-domainCaptureEnabled': boolean | undefined 'ol-domainCaptureEnabled': boolean | undefined
'ol-domainCaptureTestURL': string | undefined 'ol-domainCaptureTestURL': string | undefined
'ol-dropbox': { error: boolean; registered: boolean } 'ol-dropbox': { error: boolean; registered: boolean }
'ol-editorThemes': string[] 'ol-editorThemes': { name: string; dark: boolean }[]
'ol-email': string 'ol-email': string
'ol-emailAddressLimit': number 'ol-emailAddressLimit': number
'ol-error': { name: string } | undefined 'ol-error': { name: string } | undefined
@@ -188,7 +188,7 @@ export interface Meta {
'ol-labsExperiments': ActiveExperiment[] | undefined 'ol-labsExperiments': ActiveExperiment[] | undefined
'ol-languages': SpellCheckLanguage[] 'ol-languages': SpellCheckLanguage[]
'ol-learnedWords': string[] 'ol-learnedWords': string[]
'ol-legacyEditorThemes': string[] 'ol-legacyEditorThemes': { name: string; dark: boolean }[]
'ol-licenseQuantity'?: number 'ol-licenseQuantity'?: number
'ol-loadingText': string 'ol-loadingText': string
'ol-managedGroupSubscriptions': ManagedGroupSubscription[] 'ol-managedGroupSubscriptions': ManagedGroupSubscription[]
@@ -7,6 +7,11 @@
.file-view .no-preview { .file-view .no-preview {
color: var(--content-secondary-themed); color: var(--content-secondary-themed);
} }
.file-view .loading-panel {
color: var(--content-secondary-themed);
background: var(--bg-primary-themed);
}
} }
.file-view { .file-view {
@@ -77,6 +77,17 @@ $editor-toggler-bg-dark-color: color.adjust(
background-color: var(--bg-tertiary-themed); background-color: var(--bg-tertiary-themed);
} }
} }
.loading-panel {
background-color: var(--bg-light-primary);
color: var(--content-primary);
// Based on editor theme rather than overall theme
&.loading-panel-dark {
background-color: var(--bg-dark-primary);
color: var(--content-primary-dark);
}
}
} }
.global-alerts { .global-alerts {
@@ -571,12 +571,16 @@ describe('<EditorLeftMenu />', function () {
}) })
it('shows editor theme menu correctly', function () { it('shows editor theme menu correctly', function () {
const editorThemes = ['editortheme-1', 'editortheme-2', 'editortheme-3'] const editorThemes = [
{ name: 'editortheme-1', dark: false },
{ name: 'editortheme-2', dark: false },
{ name: 'editortheme-3', dark: false },
]
const legacyEditorThemes = [ const legacyEditorThemes = [
'legacytheme-1', { name: 'legacytheme-1', dark: false },
'legacytheme-2', { name: 'legacytheme-2', dark: false },
'legacytheme-3', { name: 'legacytheme-3', dark: false },
] ]
window.metaAttributesCache.set('ol-editorThemes', editorThemes) window.metaAttributesCache.set('ol-editorThemes', editorThemes)
@@ -6,9 +6,17 @@ import { EditorLeftMenuProvider } from '@/features/editor-left-menu/components/e
import { EditorProviders } from '../../../../helpers/editor-providers' import { EditorProviders } from '../../../../helpers/editor-providers'
describe('<SettingsEditorTheme />', function () { describe('<SettingsEditorTheme />', function () {
const editorThemes = ['editortheme-1', 'editortheme-2', 'editortheme-3'] const editorThemes = [
{ name: 'editortheme-1', dark: false },
{ name: 'editortheme-2', dark: false },
{ name: 'editortheme-3', dark: false },
]
const legacyEditorThemes = ['legacytheme-1', 'legacytheme-2', 'legacytheme-3'] const legacyEditorThemes = [
{ name: 'legacytheme-1', dark: false },
{ name: 'legacytheme-2', dark: false },
{ name: 'legacytheme-3', dark: false },
]
beforeEach(function () { beforeEach(function () {
window.metaAttributesCache.set('ol-editorThemes', editorThemes) window.metaAttributesCache.set('ol-editorThemes', editorThemes)
@@ -31,15 +39,15 @@ describe('<SettingsEditorTheme />', function () {
const select = screen.getByLabelText('Editor theme') const select = screen.getByLabelText('Editor theme')
for (const theme of editorThemes) { for (const theme of editorThemes) {
const option = within(select).getByText(theme.replace(/_/g, ' ')) const option = within(select).getByText(theme.name.replace(/_/g, ' '))
expect(option.getAttribute('value')).to.equal(theme) expect(option.getAttribute('value')).to.equal(theme.name)
} }
for (const theme of legacyEditorThemes) { for (const theme of legacyEditorThemes) {
const option = within(select).getByText( const option = within(select).getByText(
theme.replace(/_/g, ' ') + ' (Legacy)' theme.name.replace(/_/g, ' ') + ' (Legacy)'
) )
expect(option.getAttribute('value')).to.equal(theme) expect(option.getAttribute('value')).to.equal(theme.name)
} }
}) })
}) })
@@ -60,8 +60,16 @@ describe('<SettingsModal />', function () {
}, },
] ]
const editorThemes = ['editortheme-1', 'editortheme-2', 'editortheme-3'] const editorThemes = [
const legacyEditorThemes = ['legacytheme-1', 'legacytheme-2', 'legacytheme-3'] { name: 'editortheme-1', dark: false },
{ name: 'editortheme-2', dark: false },
{ name: 'editortheme-3', dark: false },
]
const legacyEditorThemes = [
{ name: 'legacytheme-1', dark: false },
{ name: 'legacytheme-2', dark: false },
{ name: 'legacytheme-3', dark: false },
]
const imageNames: ImageName[] = [ const imageNames: ImageName[] = [
{ {
@@ -7,8 +7,16 @@ import EditorThemeSetting from '@/features/ide-redesign/components/settings/appe
import userEvent from '@testing-library/user-event' import userEvent from '@testing-library/user-event'
describe('<EditorThemeSetting />', function () { describe('<EditorThemeSetting />', function () {
const editorThemes = ['editortheme-1', 'editortheme-2', 'editortheme-3'] const editorThemes = [
const legacyEditorThemes = ['legacytheme-1', 'legacytheme-2', 'legacytheme-3'] { name: 'editortheme-1', dark: false },
{ name: 'editortheme-2', dark: false },
{ name: 'editortheme-3', dark: false },
]
const legacyEditorThemes = [
{ name: 'legacytheme-1', dark: false },
{ name: 'legacytheme-2', dark: false },
{ name: 'legacytheme-3', dark: false },
]
beforeEach(function () { beforeEach(function () {
window.metaAttributesCache.set('ol-editorThemes', editorThemes) window.metaAttributesCache.set('ol-editorThemes', editorThemes)
@@ -39,25 +47,25 @@ describe('<EditorThemeSetting />', function () {
const select = screen.getByLabelText('Editor theme') const select = screen.getByLabelText('Editor theme')
for (const theme of editorThemes) { for (const theme of editorThemes) {
const option = within(select).getByText(theme.replace(/_/g, ' ')) const option = within(select).getByText(theme.name.replace(/_/g, ' '))
expect(option.getAttribute('value')).to.equal(theme) expect(option.getAttribute('value')).to.equal(theme.name)
await userEvent.selectOptions(select, [option]) await userEvent.selectOptions(select, [option])
expect( expect(
saveSettingsMock.callHistory.called(`/user/settings`, { saveSettingsMock.callHistory.called(`/user/settings`, {
body: { editorTheme: theme }, body: { editorTheme: theme.name },
}) })
).to.be.true ).to.be.true
} }
for (const theme of legacyEditorThemes) { for (const theme of legacyEditorThemes) {
const option = within(select).getByText( const option = within(select).getByText(
theme.replace(/_/g, ' ') + ' (Legacy)' theme.name.replace(/_/g, ' ') + ' (Legacy)'
) )
expect(option.getAttribute('value')).to.equal(theme) expect(option.getAttribute('value')).to.equal(theme.name)
await userEvent.selectOptions(select, [option]) await userEvent.selectOptions(select, [option])
expect( expect(
saveSettingsMock.callHistory.called(`/user/settings`, { saveSettingsMock.callHistory.called(`/user/settings`, {
body: { editorTheme: theme }, body: { editorTheme: theme.name },
}) })
).to.be.true ).to.be.true
} }