Просмотр исходного кода

Merge pull request #32330 from overleaf/mj-tabs-survey

[web] Tweaks for editor tabs

GitOrigin-RevId: fed9a500b871fa68a158c2e7ab42030117775161
Mathias Jakobsen 4 месяцев назад
Родитель
Сommit
6b01183bba

+ 2 - 0
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[]

+ 35 - 8
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<HTMLDivElement>(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 (
     <div
+      ref={tabRef}
       onDragStart={onDragStart}
       onDragOver={onDragOver}
       onDragLeave={onDragLeave}

+ 81 - 12
services/web/frontend/js/features/source-editor/components/tabs/tabs-container.tsx

@@ -1,25 +1,94 @@
 import { useFileTreeOpenContext } from '@/features/ide-react/context/file-tree-open-context'
-import { useTabsContext } from '@/features/ide-react/context/tabs-context'
+import {
+  TAB_TRANSFER_TYPE,
+  useTabsContext,
+} from '@/features/ide-react/context/tabs-context'
 import { Tab } from './tab'
+import SplitTestBadge from '@/shared/components/split-test-badge'
+import { useCallback, useMemo, useState } from 'react'
+import { throttle } from 'lodash'
+import { debugConsole } from '@/utils/debugging'
+import classNames from 'classnames'
 
 export const TabsContainer = () => {
   const { tabs, openTab, closeTab, moveTab, makeTabPermanent } =
     useTabsContext()
   const { openEntity } = useFileTreeOpenContext()
+  const [hovered, setHovered] = useState<boolean>(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 (
-    <div className="editor-tabs-container" role="tablist">
-      {tabs.map(tab => (
-        <Tab
-          key={tab.id}
-          tab={tab}
-          openTab={openTab}
-          closeTab={closeTab}
-          isSelected={openEntity?.entity._id === tab.id}
-          onTabDrop={moveTab}
-          makeTabPermanent={makeTabPermanent}
+    <div className="editor-tabs-container">
+      <div
+        className={classNames('editor-tabs-row', {
+          'editor-tabs-row-hovered': hovered,
+        })}
+        role="tablist"
+        onDragOver={onDragOver}
+        onDrop={onDrop}
+        onDragLeave={onDragLeave}
+        tabIndex={-1}
+      >
+        {tabs.map(tab => (
+          <Tab
+            key={tab.id}
+            tab={tab}
+            openTab={openTab}
+            closeTab={closeTab}
+            isSelected={openEntity?.entity._id === tab.id}
+            onTabDrop={moveTab}
+            makeTabPermanent={makeTabPermanent}
+          />
+        ))}
+      </div>
+      <div className="editor-tabs-labs-icon">
+        <SplitTestBadge
+          splitTestName="editor-tabs"
+          displayOnVariants={['enabled']}
         />
-      ))}
+      </div>
     </div>
   )
 }

+ 27 - 2
services/web/frontend/stylesheets/pages/editor/tabs.scss

@@ -3,15 +3,40 @@
 }
 
 .editor-tabs-container {
+  display: flex;
+  flex-direction: row;
+  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;
-  border-bottom: 1px solid var(--border-divider-themed);
   margin: 0;
-  flex: 0 0 auto;
+  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 {