Merge pull request #33687 from overleaf/mj-temporary-tabs-fix

[web] Only consider real key presses to make tab permanent

GitOrigin-RevId: 50ab453445e111de2b317f50470f9f4eec39a66f
This commit is contained in:
Mathias Jakobsen
2026-05-15 08:08:28 +00:00
committed by Copybot
parent 6538c00742
commit ac961f1d40
4 changed files with 105 additions and 7 deletions
@@ -1,4 +1,4 @@
import React, { FC, useEffect, useRef } from 'react'
import React, { FC, useEffect, useRef, useState } from 'react'
import { EditorProviders } from '../../../helpers/editor-providers'
import { TabsContainer } from '../../../../../frontend/js/features/source-editor/components/tabs/tabs-container'
import {
@@ -11,6 +11,13 @@ import {
} from '@/features/ide-react/context/editor-manager-context'
import { TAB_TRANSFER_TYPE } from '@/features/ide-react/context/tabs-context'
import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
import {
EditorViewContext,
useEditorViewContext,
} from '@/features/ide-react/context/editor-view-context'
import { EditorView } from '@codemirror/view'
import { EditorState, Transaction } from '@codemirror/state'
import { tabsListener } from '@/features/source-editor/extensions/tabs-listener'
const DOC_IDS = {
main: 'doc-main-id',
@@ -120,6 +127,53 @@ function makeEditorManagerProvider() {
return EditorManagerProvider
}
function makeEditorViewProvider() {
const EditorViewProvider: FC<React.PropsWithChildren> = ({ children }) => {
const parentRef = useRef<HTMLDivElement>(null)
const [view, setView] = useState<EditorView | null>(null)
useEffect(() => {
if (!parentRef.current) return
const editorView = new EditorView({
state: EditorState.create({
extensions: [
tabsListener(),
EditorView.contentAttributes.of({
'data-testid': 'mock-editor-view',
}),
],
}),
parent: parentRef.current,
})
setView(editorView)
return () => editorView.destroy()
}, [])
return (
<EditorViewContext.Provider value={{ view, setView: () => {} }}>
{children}
<div ref={parentRef} />
</EditorViewContext.Provider>
)
}
return EditorViewProvider
}
function RemoteChangeButton() {
const { view } = useEditorViewContext()
return (
<button
type="button"
onClick={() =>
view?.dispatch({
changes: { from: 0, insert: 'remote text' },
annotations: Transaction.remote.of(true),
})
}
>
Add a remote change
</button>
)
}
// Rendered inside the provider tree to call handleFileTreeSelect() when a
// custom DOM event fires. Also triggers handleFileTreeInit() on mount.
function FileSelectionDriver({
@@ -188,10 +242,12 @@ describe('File Tabs', function () {
userSettings={options?.userSettings}
providers={{
EditorManagerProvider: makeEditorManagerProvider(),
EditorViewProvider: makeEditorViewProvider(),
}}
>
<FileSelectionDriver />
<TabsContainer />
<RemoteChangeButton />
</EditorProviders>
)
}
@@ -216,6 +272,8 @@ describe('File Tabs', function () {
})
mountTabs()
cy.findByTestId('mock-editor-view').as('editorView')
})
describe('Initial file selection', function () {
@@ -228,6 +286,7 @@ describe('File Tabs', function () {
rootDocId={DOC_IDS.main}
providers={{
EditorManagerProvider: makeEditorManagerProvider(),
EditorViewProvider: makeEditorViewProvider(),
}}
>
<FileSelectionDriver
@@ -266,7 +325,7 @@ describe('File Tabs', function () {
cy.findByRole('tab', { name: /main\.tex/ }).should('exist')
// Make main permanent (keypress) so selecting another file doesn't replace it
cy.get('body').type('a')
cy.get('@editorView').type('a')
// Select another file
cy.then(() => selectDoc(DOC_IDS.intro))
@@ -307,7 +366,7 @@ describe('File Tabs', function () {
'tab-temporary'
)
cy.get('body').type('a')
cy.get('@editorView').type('a')
cy.findByRole('tab', { name: /main\.tex/ }).should(
'not.have.class',
@@ -321,7 +380,7 @@ describe('File Tabs', function () {
cy.findByRole('tab', { name: /main\.tex/ }).should('exist')
// Make main permanent
cy.get('body').type('a')
cy.get('@editorView').type('a')
// Open intro (temporary)
cy.then(() => selectDoc(DOC_IDS.intro))
@@ -338,6 +397,21 @@ describe('File Tabs', function () {
cy.findByRole('tab', { name: /main\.tex/ }).should('exist')
})
it('does not make a temporary tab permanent on remote changes', function () {
cy.then(() => selectDoc(DOC_IDS.main))
cy.findByRole('tab', { name: /main\.tex/ }).should(
'have.class',
'tab-temporary'
)
cy.findByRole('button', { name: 'Add a remote change' }).click()
cy.findByRole('tab', { name: /main\.tex/ }).should(
'have.class',
'tab-temporary'
)
})
it('makes a temporary tab permanent on double-click', function () {
cy.then(() => selectDoc(DOC_IDS.main))
cy.findByRole('tab', { name: /main\.tex/ }).should(
@@ -902,7 +976,7 @@ describe('File Tabs', function () {
for (let i = 1; i <= 10; i++) {
const id = `ch${i}`
cy.then(() => selectEntity(makeDocEntity(id, `chapter-${i}.tex`)))
cy.get('body').type('a')
cy.get('@editorView').type('a')
cy.findByRole('tab', { name: new RegExp(`chapter-${i}.tex`) }).should(
'be.visible'
)