From 6b01183bba15c8091504e603aedb5366e9678787 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Mon, 23 Mar 2026 09:35:57 +0000 Subject: [PATCH] Merge pull request #32330 from overleaf/mj-tabs-survey [web] Tweaks for editor tabs GitOrigin-RevId: fed9a500b871fa68a158c2e7ab42030117775161 --- .../ide-react/context/tabs-context.tsx | 2 + .../source-editor/components/tabs/tab.tsx | 43 +++++++-- .../components/tabs/tabs-container.tsx | 93 ++++++++++++++++--- .../stylesheets/pages/editor/tabs.scss | 33 ++++++- 4 files changed, 147 insertions(+), 24 deletions(-) diff --git a/services/web/frontend/js/features/ide-react/context/tabs-context.tsx b/services/web/frontend/js/features/ide-react/context/tabs-context.tsx index 2b2189125b..0be66bc1ff 100644 --- a/services/web/frontend/js/features/ide-react/context/tabs-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/tabs-context.tsx @@ -20,6 +20,8 @@ export type EditorFileTab = { lifetime: Lifetime } +export const TAB_TRANSFER_TYPE = 'text/x.tab-id' + const TabsContext = React.createContext< | { tabs: EditorFileTab[] diff --git a/services/web/frontend/js/features/source-editor/components/tabs/tab.tsx b/services/web/frontend/js/features/source-editor/components/tabs/tab.tsx index 586e629634..e0ea074cee 100644 --- a/services/web/frontend/js/features/source-editor/components/tabs/tab.tsx +++ b/services/web/frontend/js/features/source-editor/components/tabs/tab.tsx @@ -1,9 +1,20 @@ -import { EditorFileTab } from '@/features/ide-react/context/tabs-context' +import { + EditorFileTab, + TAB_TRANSFER_TYPE, +} from '@/features/ide-react/context/tabs-context' import MaterialIcon from '@/shared/components/material-icon' import { debugConsole } from '@/utils/debugging' import classNames from 'classnames' import { throttle } from 'lodash' -import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { + memo, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' import { useTranslation } from 'react-i18next' type TabProps = { @@ -31,8 +42,6 @@ function getSideOfTargetFromEvent( } } -const TAB_TRANSFER_TYPE = 'text/x.tab-id' - export const Tab = memo(function Tab({ tab, openTab, @@ -42,12 +51,14 @@ export const Tab = memo(function Tab({ onTabDrop, }: TabProps) { const { t } = useTranslation() + const tabRef = useRef(null) const [dropTargetPosition, setDropTargetPosition] = useState< 'left' | 'right' | null >(null) const onDragStart = useCallback( (e: React.DragEvent) => { + e.stopPropagation() e.dataTransfer.setData(TAB_TRANSFER_TYPE, tab.id) e.dataTransfer.effectAllowed = 'move' }, @@ -65,22 +76,28 @@ export const Tab = memo(function Tab({ const onDragOver = useCallback( (e: React.DragEvent) => { e.preventDefault() + e.stopPropagation() e.dataTransfer.dropEffect = 'move' throttledOnDragOver(e.currentTarget, e.clientX) }, [throttledOnDragOver] ) - const onDragLeave = useCallback(() => { - throttledOnDragOver.cancel() - setDropTargetPosition(null) - }, [throttledOnDragOver]) + const onDragLeave = useCallback( + (e: React.DragEvent) => { + e.stopPropagation() + throttledOnDragOver.cancel() + setDropTargetPosition(null) + }, + [throttledOnDragOver] + ) const onDrop = useCallback( (e: React.DragEvent) => { throttledOnDragOver.cancel() setDropTargetPosition(null) e.preventDefault() + e.stopPropagation() const draggedTabId = e.dataTransfer.getData(TAB_TRANSFER_TYPE) if (!draggedTabId) { debugConsole.warn('No dragged tab id found in dataTransfer') @@ -135,6 +152,15 @@ export const Tab = memo(function Tab({ [closeTab, tab] ) + useLayoutEffect(() => { + if (isSelected && tabRef.current) { + tabRef.current.scrollIntoView({ + block: 'nearest', + inline: 'nearest', + }) + } + }, [isSelected]) + useEffect(() => { if (isSelected && tab.lifetime === 'temporary') { const handler = () => { @@ -149,6 +175,7 @@ export const Tab = memo(function Tab({ return (
{ const { tabs, openTab, closeTab, moveTab, makeTabPermanent } = useTabsContext() const { openEntity } = useFileTreeOpenContext() + const [hovered, setHovered] = useState(false) + + const throttledOnDragOver = useMemo( + () => + throttle(() => { + setHovered(true) + }, 50), + [] + ) + + const onDragOver = useCallback( + (e: React.DragEvent) => { + e.preventDefault() + throttledOnDragOver() + }, + [throttledOnDragOver] + ) + + const onDrop = useCallback( + (e: React.DragEvent) => { + throttledOnDragOver.cancel() + e.stopPropagation() + e.preventDefault() + setHovered(false) + + const draggedTabId = e.dataTransfer.getData(TAB_TRANSFER_TYPE) + if (!draggedTabId) { + debugConsole.warn('No dragged tab id found in dataTransfer') + return + } + const targetTabId = tabs[tabs.length - 1]?.id + if (!targetTabId) { + debugConsole.warn('No target tab id found for drop') + return + } + moveTab(draggedTabId, targetTabId, 'right') + }, + [tabs, moveTab, throttledOnDragOver] + ) + + const onDragLeave = useCallback(() => { + throttledOnDragOver.cancel() + setHovered(false) + }, [throttledOnDragOver]) return ( -
- {tabs.map(tab => ( - +
+ {tabs.map(tab => ( + + ))} +
+
+ - ))} +
) } diff --git a/services/web/frontend/stylesheets/pages/editor/tabs.scss b/services/web/frontend/stylesheets/pages/editor/tabs.scss index 7db6d2b5cd..27374f998a 100644 --- a/services/web/frontend/stylesheets/pages/editor/tabs.scss +++ b/services/web/frontend/stylesheets/pages/editor/tabs.scss @@ -5,13 +5,38 @@ .editor-tabs-container { display: flex; flex-direction: row; - list-style: none; - padding: 0; - border-bottom: 1px solid var(--border-divider-themed); - margin: 0; + align-items: center; + gap: var(--spacing-02); flex: 0 0 auto; background: var(--bg-primary-themed); + justify-content: space-between; + border-bottom: 1px solid var(--border-divider-themed); + + .editor-tabs-labs-icon { + flex: 0 0 auto; + padding: 0 var(--spacing-02); + + .material-symbols { + font-size: 16px; + } + } +} + +.editor-tabs-row { + display: flex; + flex-direction: row; + list-style: none; + padding: 0; + margin: 0; + flex: 1; + background: var(--bg-primary-themed); overflow-x: auto; + + &.editor-tabs-row-hovered { + .editor-file-tab:last-child { + border-right-color: var(--border-primary-themed); + } + } } .editor-file-tab {