mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-14 06:03:45 +01:00
Merge pull request #34162 from overleaf/mj-tabs-close-to-right
[web] Context menu updates for tabs GitOrigin-RevId: 09dbecaf119237adfb9b779a46bf76103c12de8e
This commit is contained in:
committed by
Copybot
parent
20f03d75fe
commit
ddfff06ef8
@@ -311,8 +311,8 @@
|
||||
"clone_with_git": "",
|
||||
"close": "",
|
||||
"close_dialog": "",
|
||||
"close_others": "",
|
||||
"close_tab": "",
|
||||
"close_other_tabs": "",
|
||||
"close_tabs_to_the_right": "",
|
||||
"clsi_maintenance": "",
|
||||
"clsi_unavailable": "",
|
||||
"code": "",
|
||||
@@ -2014,6 +2014,7 @@
|
||||
"syntax_checks": "",
|
||||
"syntax_validation": "",
|
||||
"tab_connecting": "",
|
||||
"tab_settings": "",
|
||||
"table": "",
|
||||
"tabs_open_in_preview_mode_until_you_interact_with_them": "",
|
||||
"tag_color": "",
|
||||
|
||||
@@ -48,6 +48,7 @@ const TabsContext = React.createContext<
|
||||
closeTab: (id: string) => void
|
||||
closeOtherTabs: (id: string) => void
|
||||
makeTabPermanent: (id: string) => void
|
||||
closeToRight: (id: string) => void
|
||||
moveTab: (
|
||||
sourceTabId: string,
|
||||
targetTabId: string,
|
||||
@@ -174,6 +175,32 @@ export const TabsProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
[openEntity, openTab, setOpenTabs]
|
||||
)
|
||||
|
||||
const closeToRight = useCallback(
|
||||
async (id: string) => {
|
||||
const tabIndex = openTabs.findIndex(tab => tab.id === id)
|
||||
if (tabIndex === -1) {
|
||||
debugConsole.warn(
|
||||
'Attempting to close to right on tab that is not open'
|
||||
)
|
||||
return
|
||||
}
|
||||
const openTabIndex = openTabs.findIndex(
|
||||
tab => tab.id === openEntity?.entity._id
|
||||
)
|
||||
if (openTabIndex === -1 || openTabIndex > tabIndex) {
|
||||
await openTab(id)
|
||||
}
|
||||
setOpenTabs(current => {
|
||||
const currentIndex = current.findIndex(tab => tab.id === id)
|
||||
if (currentIndex === -1) {
|
||||
return current
|
||||
}
|
||||
return current.slice(0, currentIndex + 1)
|
||||
})
|
||||
},
|
||||
[setOpenTabs, openTabs, openEntity, openTab]
|
||||
)
|
||||
|
||||
const moveTab = useCallback(
|
||||
(sourceTabId: string, targetTabId: string, position: 'left' | 'right') => {
|
||||
debugConsole.log({ sourceTabId, targetTabId, position })
|
||||
@@ -269,6 +296,7 @@ export const TabsProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
makeTabPermanent,
|
||||
contextMenuTarget,
|
||||
setContextMenuTarget,
|
||||
closeToRight,
|
||||
}),
|
||||
[
|
||||
tabs,
|
||||
@@ -279,6 +307,7 @@ export const TabsProvider: FC<React.PropsWithChildren> = ({ children }) => {
|
||||
makeTabPermanent,
|
||||
contextMenuTarget,
|
||||
setContextMenuTarget,
|
||||
closeToRight,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -24,6 +24,10 @@ export default function useFocusOnSetting() {
|
||||
|
||||
const settingToFocusElt: HTMLElement | null =
|
||||
settingElt?.querySelector('input, select, button') ?? settingElt
|
||||
settingElt?.classList.add('ide-setting-highlighted')
|
||||
setTimeout(() => {
|
||||
settingElt?.classList.remove('ide-setting-highlighted')
|
||||
}, 2000)
|
||||
|
||||
setActiveTab(newActiveTab)
|
||||
setEltToScrollTo({ tab: newActiveTab, element: settingToFocusElt })
|
||||
|
||||
@@ -231,7 +231,7 @@ export const Tab = memo(function Tab({
|
||||
<button
|
||||
onClick={onCloseClick}
|
||||
className="editor-file-tab-close-action"
|
||||
aria-label={t('close_tab')}
|
||||
aria-label={t('close')}
|
||||
>
|
||||
<MaterialIcon type="close" />
|
||||
</button>
|
||||
|
||||
+33
-2
@@ -3,6 +3,7 @@ import ReactDOM from 'react-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownDivider,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
} from '@/shared/components/dropdown/dropdown-menu'
|
||||
@@ -14,6 +15,7 @@ export function TabsContextMenu() {
|
||||
tabs,
|
||||
closeTab,
|
||||
closeOtherTabs,
|
||||
closeToRight,
|
||||
contextMenuTarget,
|
||||
setContextMenuTarget,
|
||||
} = useTabsContext()
|
||||
@@ -89,7 +91,7 @@ export function TabsContextMenu() {
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{t('close_tab')}
|
||||
{t('close')}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
disabled={tabs.length <= 1}
|
||||
@@ -99,7 +101,36 @@ export function TabsContextMenu() {
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{t('close_others')}
|
||||
{t('close_other_tabs')}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
disabled={tabs[tabs.length - 1]?.id === contextMenuTarget.tabId}
|
||||
as="button"
|
||||
onClick={() => {
|
||||
closeToRight(contextMenuTarget.tabId)
|
||||
close()
|
||||
}}
|
||||
>
|
||||
{t('close_tabs_to_the_right')}
|
||||
</DropdownItem>
|
||||
<DropdownDivider />
|
||||
<DropdownItem
|
||||
as="button"
|
||||
onClick={() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ui.toggle-left-menu', { detail: true })
|
||||
)
|
||||
// focus the tab setting
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('ui.focus-setting', {
|
||||
detail: 'previewTabs',
|
||||
})
|
||||
)
|
||||
close()
|
||||
}}
|
||||
leadingIcon="settings"
|
||||
>
|
||||
{t('tab_settings')}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
max-height: 75%;
|
||||
height: 500px;
|
||||
width: 100%;
|
||||
padding: 0 var(--spacing-06);
|
||||
padding: 0 var(--spacing-04);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
line-height: var(--line-height-01);
|
||||
padding-bottom: var(--spacing-06);
|
||||
border-bottom: 1px solid var(--border-divider);
|
||||
margin: 0 var(--spacing-04);
|
||||
}
|
||||
|
||||
.ide-setting {
|
||||
@@ -72,6 +73,9 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 48px;
|
||||
padding: 0 var(--spacing-02);
|
||||
margin: 0 var(--spacing-02);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.ide-dropdown-setting {
|
||||
@@ -96,7 +100,7 @@
|
||||
min-width: 160px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: var(--spacing-06);
|
||||
margin-left: var(--spacing-05);
|
||||
}
|
||||
|
||||
.ide-radio-setting-options {
|
||||
@@ -116,3 +120,7 @@
|
||||
align-items: center;
|
||||
gap: var(--spacing-05);
|
||||
}
|
||||
|
||||
.ide-setting-highlighted {
|
||||
box-shadow: 0 1px 10px 0 rgb(102 175 233 / 80%);
|
||||
}
|
||||
|
||||
@@ -409,8 +409,8 @@
|
||||
"clone_with_git": "Clone with Git",
|
||||
"close": "Close",
|
||||
"close_dialog": "Close dialog",
|
||||
"close_others": "Close others",
|
||||
"close_tab": "Close tab",
|
||||
"close_other_tabs": "Close other tabs",
|
||||
"close_tabs_to_the_right": "Close tabs to the right",
|
||||
"clsi_maintenance": "The compile servers are down for maintenance, and will be back shortly.",
|
||||
"clsi_unavailable": "Sorry, the compile server for your project was temporarily unavailable. Please try again in a few moments.",
|
||||
"cn": "Chinese (Simplified)",
|
||||
@@ -2602,6 +2602,7 @@
|
||||
"syntax_checks": "Syntax checks",
|
||||
"syntax_validation": "Code check",
|
||||
"tab_connecting": "Connecting with the editor",
|
||||
"tab_settings": "Tab settings…",
|
||||
"table": "Table",
|
||||
"table_generator": "Table Generator",
|
||||
"tabs_open_in_preview_mode_until_you_interact_with_them": "Tabs open in preview mode until you interact with them",
|
||||
|
||||
@@ -450,7 +450,7 @@ describe('File Tabs', function () {
|
||||
|
||||
// Close intro
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).within(() => {
|
||||
cy.findByRole('button', { name: 'Close tab' }).click()
|
||||
cy.findByRole('button', { name: 'Close' }).click()
|
||||
})
|
||||
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).should('not.exist')
|
||||
@@ -463,7 +463,7 @@ describe('File Tabs', function () {
|
||||
|
||||
// Attempt to close the only tab
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).within(() => {
|
||||
cy.findByRole('button', { name: 'Close tab' }).click()
|
||||
cy.findByRole('button', { name: 'Close' }).click()
|
||||
})
|
||||
|
||||
// Tab must still exist
|
||||
@@ -481,7 +481,7 @@ describe('File Tabs', function () {
|
||||
|
||||
// Close the currently selected tab (appendix — the last one selected)
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).within(() => {
|
||||
cy.findByRole('button', { name: 'Close tab' }).click()
|
||||
cy.findByRole('button', { name: 'Close' }).click()
|
||||
})
|
||||
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).should('not.exist')
|
||||
@@ -514,23 +514,27 @@ describe('File Tabs', function () {
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
|
||||
cy.findByRole('menu').should('exist')
|
||||
cy.findByRole('menuitem', { name: 'Close tab' }).should('exist')
|
||||
cy.findByRole('menuitem', { name: 'Close others' }).should('exist')
|
||||
cy.findByRole('menuitem', { name: 'Close' }).should('exist')
|
||||
cy.findByRole('menuitem', { name: 'Close other tabs' }).should('exist')
|
||||
cy.findByRole('menuitem', { name: 'Close tabs to the right' }).should(
|
||||
'exist'
|
||||
)
|
||||
cy.findByRole('menuitem', { name: 'Tab settings…' }).should('exist')
|
||||
})
|
||||
|
||||
it('closes the clicked tab via "Close tab"', function () {
|
||||
it('closes the clicked tab via "Close"', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Close tab' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Close' }).click()
|
||||
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).should('not.exist')
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).should('exist')
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
})
|
||||
|
||||
it('closes all other tabs via "Close others"', function () {
|
||||
it('closes all other tabs via "Close other tabs"', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
cy.then(() => selectDoc(DOC_IDS.appendix))
|
||||
@@ -538,7 +542,7 @@ describe('File Tabs', function () {
|
||||
cy.findAllByRole('tab').should('have.length', 3)
|
||||
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Close others' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Close other tabs' }).click()
|
||||
|
||||
cy.findAllByRole('tab').should('have.length', 1)
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).should('exist')
|
||||
@@ -551,26 +555,132 @@ describe('File Tabs', function () {
|
||||
|
||||
// appendix is the currently active tab; close others from intro
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Close others' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Close other tabs' }).click()
|
||||
|
||||
cy.get('@openDocWithId').should('have.been.calledWith', DOC_IDS.intro)
|
||||
})
|
||||
|
||||
it('disables both items when only one tab is open', function () {
|
||||
it('closes tabs to the right of the target via "Close tabs to the right"', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
cy.then(() => selectDoc(DOC_IDS.appendix))
|
||||
|
||||
cy.findAllByRole('tab').should('have.length', 3)
|
||||
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Close tabs to the right' }).click()
|
||||
|
||||
cy.findAllByRole('tab').should('have.length', 2)
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).should('exist')
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).should('exist')
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).should('not.exist')
|
||||
})
|
||||
|
||||
it('navigates to the target tab when the active tab is closed to the right', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
cy.then(() => selectDoc(DOC_IDS.appendix))
|
||||
|
||||
// appendix is the active tab; closing to the right of intro removes it
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Close tabs to the right' }).click()
|
||||
|
||||
cy.get('@openDocWithId').should('have.been.calledWith', DOC_IDS.intro)
|
||||
})
|
||||
|
||||
it('keeps the active tab selected when it is to the left of the target', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
cy.then(() => selectDoc(DOC_IDS.appendix))
|
||||
|
||||
// Re-select main so it becomes the active tab again (still at index 0)
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
|
||||
// Closing to the right of intro removes appendix but not the active tab
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Close tabs to the right' }).click()
|
||||
|
||||
cy.findAllByRole('tab').should('have.length', 2)
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).should('not.exist')
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).should(
|
||||
'have.attr',
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
|
||||
it('disables the close actions when only one tab is open', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
|
||||
cy.findByRole('tab', { name: /main\.tex/ }).rightclick()
|
||||
|
||||
cy.findByRole('menuitem', { name: 'Close tab' }).should(
|
||||
cy.findByRole('menuitem', { name: 'Close' }).should(
|
||||
'have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
cy.findByRole('menuitem', { name: 'Close others' }).should(
|
||||
cy.findByRole('menuitem', { name: 'Close other tabs' }).should(
|
||||
'have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
cy.findByRole('menuitem', { name: 'Close tabs to the right' }).should(
|
||||
'have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
|
||||
it('disables "Close tabs to the right" when the target is the last tab', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
cy.then(() => selectDoc(DOC_IDS.appendix))
|
||||
|
||||
// appendix is the rightmost tab, so there is nothing to close to its right
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).rightclick()
|
||||
|
||||
cy.findByRole('menuitem', { name: 'Close tabs to the right' }).should(
|
||||
'have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
// The other actions remain enabled with more than one tab open
|
||||
cy.findByRole('menuitem', { name: 'Close' }).should(
|
||||
'not.have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
cy.findByRole('menuitem', { name: 'Close other tabs' }).should(
|
||||
'not.have.attr',
|
||||
'aria-disabled',
|
||||
'true'
|
||||
)
|
||||
})
|
||||
|
||||
it('opens tab settings via "Tab settings…"', function () {
|
||||
cy.then(() => selectDoc(DOC_IDS.main))
|
||||
cy.then(() => selectDoc(DOC_IDS.intro))
|
||||
|
||||
cy.window().then(win => {
|
||||
cy.spy(win, 'dispatchEvent').as('dispatchEvent')
|
||||
})
|
||||
|
||||
cy.findByRole('tab', { name: /intro\.tex/ }).rightclick()
|
||||
cy.findByRole('menuitem', { name: 'Tab settings…' }).click()
|
||||
|
||||
// opens the left settings menu...
|
||||
cy.get('@dispatchEvent').should(
|
||||
'have.been.calledWithMatch',
|
||||
Cypress.sinon.match({ type: 'ui.toggle-left-menu', detail: true })
|
||||
)
|
||||
// ...and focuses the previewTabs setting
|
||||
cy.get('@dispatchEvent').should(
|
||||
'have.been.calledWithMatch',
|
||||
Cypress.sinon.match({ type: 'ui.focus-setting', detail: 'previewTabs' })
|
||||
)
|
||||
|
||||
// and the context menu closes
|
||||
cy.findByRole('menu').should('not.exist')
|
||||
})
|
||||
|
||||
it('closes the menu on Escape', function () {
|
||||
@@ -631,7 +741,7 @@ describe('File Tabs', function () {
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).rightclick({
|
||||
force: true,
|
||||
})
|
||||
cy.findByRole('menuitem', { name: 'Close others' }).click()
|
||||
cy.findByRole('menuitem', { name: 'Close other tabs' }).click()
|
||||
|
||||
cy.findAllByRole('tab').should('have.length', 1)
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).should('exist')
|
||||
@@ -1040,7 +1150,7 @@ describe('File Tabs', function () {
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).should('exist')
|
||||
|
||||
cy.findByRole('tab', { name: /appendix\.tex/ }).within(() => {
|
||||
cy.findByRole('button', { name: 'Close tab' }).click()
|
||||
cy.findByRole('button', { name: 'Close' }).click()
|
||||
})
|
||||
|
||||
cy.findAllByRole('tab').should('have.length', 1)
|
||||
|
||||
Reference in New Issue
Block a user