diff --git a/frontend/app/tab/tab.less b/frontend/app/tab/tab.less index 8036c069..fb238d26 100644 --- a/frontend/app/tab/tab.less +++ b/frontend/app/tab/tab.less @@ -81,3 +81,18 @@ visibility: visible; } } + +@keyframes expandWidthAndFadeIn { + from { + width: var(--initial-tab-width); + opacity: 0; + } + to { + width: var(--final-tab-width); + opacity: 1; + } +} + +.tab.new-tab { + animation: expandWidthAndFadeIn 0.1s forwards; +} diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 75915e0d..edf31f15 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -7,7 +7,7 @@ import * as services from "@/store/services"; import * as WOS from "@/store/wos"; import { clsx } from "clsx"; import * as React from "react"; -import { forwardRef, useEffect, useRef, useState } from "react"; +import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from "react"; import { atoms, globalStore } from "@/app/store/global"; import "./tab.less"; @@ -18,6 +18,8 @@ interface TabProps { isFirst: boolean; isBeforeActive: boolean; isDragging: boolean; + tabWidth: number; + isNew: boolean; onSelect: () => void; onClose: (event: React.MouseEvent | null) => void; onDragStart: (event: React.MouseEvent) => void; @@ -26,7 +28,22 @@ interface TabProps { const Tab = React.memo( forwardRef( - ({ id, active, isFirst, isBeforeActive, isDragging, onLoaded, onSelect, onClose, onDragStart }, ref) => { + ( + { + id, + active, + isFirst, + isBeforeActive, + isDragging, + tabWidth, + isNew, + onLoaded, + onSelect, + onClose, + onDragStart, + }, + ref + ) => { const [tabData, tabLoading] = WOS.useWaveObjectValue(WOS.makeORef("tab", id)); const [originalName, setOriginalName] = useState(""); const [isEditable, setIsEditable] = useState(false); @@ -34,6 +51,9 @@ const Tab = React.memo( const editableRef = useRef(null); const editableTimeoutRef = useRef(); const loadedRef = useRef(false); + const tabRef = useRef(null); + + useImperativeHandle(ref, () => tabRef.current as HTMLDivElement); useEffect(() => { if (tabData?.name) { @@ -103,6 +123,14 @@ const Tab = React.memo( } }, [onLoaded]); + useEffect(() => { + if (tabRef.current && isNew) { + const initialWidth = `${(tabWidth / 3) * 2}px`; + tabRef.current.style.setProperty("--initial-tab-width", initialWidth); + tabRef.current.style.setProperty("--final-tab-width", `${tabWidth}px`); + } + }, [isNew, tabWidth]); + // Prevent drag from being triggered on mousedown const handleMouseDownOnClose = (event: React.MouseEvent) => { event.stopPropagation(); @@ -149,8 +177,13 @@ const Tab = React.memo( return (
{ const [tabsLoaded, setTabsLoaded] = useState({}); // const [scrollable, setScrollable] = useState(false); // const [tabWidth, setTabWidth] = useState(TAB_DEFAULT_WIDTH); + const [newTabId, setNewTabId] = useState(null); + const tabbarWrapperRef = useRef(null); const tabBarRef = useRef(null); const tabsWrapperRef = useRef(null); const tabRefs = useRef[]>([]); const addBtnRef = useRef(null); const draggingTimeoutIdRef = useRef(null); const scrollToNewTabTimeoutIdRef = useRef(null); + const newTabIdTimeoutIdRef = useRef(null); const draggingRemovedRef = useRef(false); const draggingTabDataRef = useRef({ tabId: "", @@ -63,6 +67,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { }); const osInstanceRef = useRef(null); const draggerRightRef = useRef(null); + const draggerLeftRef = useRef(null); const tabWidthRef = useRef(TAB_DEFAULT_WIDTH); const scrollableRef = useRef(false); @@ -94,7 +99,7 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { } }, [workspace, tabIds]); - const updateTabPositions = useCallback(() => { + const saveTabsPosition = useCallback(() => { const tabs = tabRefs.current; if (tabs === null) return; @@ -111,16 +116,17 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { setDragStartPositions(newStartPositions); }, []); - // const debouncedSetTabWidth = debounce((width) => setTabWidth(width), 100); - // const debouncedSetScrollable = debounce((scrollable) => setScrollable(scrollable), 100); - const debouncedUpdateTabPositions = debounce(100, () => updateTabPositions()); + const debouncedSaveTabsPosition = debounce(100, () => saveTabsPosition()); - const handleResizeTabs = useCallback(() => { + const updateSizeAndPosition = (animate?: boolean) => { const tabBar = tabBarRef.current; if (tabBar === null) return; - const tabBarRect = tabBar.getBoundingClientRect(); - const tabBarWidth = tabBarRect.width; + const tabbarWrapperWidth = tabbarWrapperRef.current.getBoundingClientRect().width; + const windowDragLeftWidth = draggerLeftRef.current.getBoundingClientRect().width; + const addBtnWidth = addBtnRef.current.getBoundingClientRect().width; + const spaceForTabs = tabbarWrapperWidth - (windowDragLeftWidth + DRAGGER_RIGHT_MIN_WIDTH + addBtnWidth); + const numberOfTabs = tabIds.length; const totalDefaultTabWidth = numberOfTabs * TAB_DEFAULT_WIDTH; const minTotalTabWidth = numberOfTabs * TAB_MIN_WIDTH; @@ -129,13 +135,15 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { let newTabWidth = tabWidth; let newScrollable = scrollable; - if (minTotalTabWidth > tabBarWidth) { + console.log("spaceForTabs", spaceForTabs, minTotalTabWidth); + + if (minTotalTabWidth > spaceForTabs) { // Case where tabs cannot shrink further, make the tab bar scrollable newTabWidth = TAB_MIN_WIDTH; newScrollable = true; - } else if (totalDefaultTabWidth > tabBarWidth) { + } else if (totalDefaultTabWidth > spaceForTabs) { // Case where resizing is needed due to limited container width - newTabWidth = tabBarWidth / numberOfTabs; + newTabWidth = spaceForTabs / numberOfTabs; newScrollable = false; } else { // Case where tabs were previously shrunk or there is enough space for default width tabs @@ -146,6 +154,11 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { // Apply the calculated width and position to all tabs tabRefs.current.forEach((ref, index) => { if (ref.current) { + if (animate) { + ref.current.classList.add("animate"); + } else { + ref.current.classList.remove("animate"); + } ref.current.style.width = `${newTabWidth}px`; ref.current.style.transform = `translate3d(${index * newTabWidth}px,0,0)`; } @@ -167,30 +180,12 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { osInstanceRef.current.destroy(); } } + }; - // Update Add Tab button position if needed - const addButton = addBtnRef.current; - const lastTabRef = tabRefs.current[tabRefs.current.length - 1]; - if (addButton && lastTabRef && lastTabRef.current) { - const lastTabRect = lastTabRef.current.getBoundingClientRect(); - addButton.style.position = "absolute"; - if (newScrollable) { - addButton.style.transform = `translate3d(${tabBarRect.left + tabBarWidth + 1}px,0,0)`; - } else { - addButton.style.transform = `translate3d(${lastTabRect.right + 1}px,0,0)`; - } - } - // Update dragger right position if needed - const draggerRight = draggerRightRef.current; - if (draggerRight && addButton) { - const addButtonRect = addButton.getBoundingClientRect(); - const targetPos = addButtonRect.left + addButtonRect.width; - draggerRight.style.transform = `translate3d(${targetPos}px,0,0)`; - draggerRight.style.width = `${document.documentElement.offsetWidth - targetPos}px`; - } - - debouncedUpdateTabPositions(); - }, [tabIds, isFullScreen]); + const handleResizeTabs = useCallback(() => { + updateSizeAndPosition(); + debouncedSaveTabsPosition(); + }, [tabIds, newTabId, isFullScreen]); useEffect(() => { window.addEventListener("resize", () => handleResizeTabs()); @@ -203,10 +198,10 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { // Check if all tabs are loaded const allLoaded = tabIds.length > 0 && tabIds.every((id) => tabsLoaded[id]); if (allLoaded) { - updateTabPositions(); - handleResizeTabs(); + updateSizeAndPosition(newTabId === null); + saveTabsPosition(); } - }, [tabIds, tabsLoaded, handleResizeTabs, updateTabPositions]); + }, [tabIds, tabsLoaded, newTabId, handleResizeTabs, saveTabsPosition]); // Make sure timeouts are cleared when component is unmounted useEffect(() => { @@ -217,6 +212,9 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { if (scrollToNewTabTimeoutIdRef.current) { clearTimeout(scrollToNewTabTimeoutIdRef.current); } + if (newTabIdTimeoutIdRef.current) { + clearTimeout(newTabIdTimeoutIdRef.current); + } }; }, []); @@ -402,10 +400,10 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { }; const handleDragStart = useCallback( - (event: React.MouseEvent, name: string, ref: React.RefObject) => { + (event: React.MouseEvent, tabId: string, ref: React.RefObject) => { if (event.button !== 0) return; - const tabIndex = tabIds.indexOf(name); + const tabIndex = tabIds.indexOf(tabId); const tabStartX = dragStartPositions[tabIndex]; // Starting X position of the tab if (ref.current) { @@ -438,8 +436,13 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { const handleAddTab = () => { const newTabName = `T${tabIds.length + 1}`; - setTabIds([...tabIds, newTabName]); - services.ObjectService.AddTabToWorkspace(newTabName, true); + services.ObjectService.AddTabToWorkspace(newTabName, true).then((tabId) => { + setTabIds([...tabIds, tabId]); + setNewTabId(tabId); + }); + services.ObjectService.GetObject; + tabsWrapperRef.current.style.transition; + tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.1s ease"); scrollToNewTabTimeoutIdRef.current = setTimeout(() => { if (scrollableRef.current) { @@ -447,11 +450,16 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { viewport.scrollLeft = tabIds.length * tabWidthRef.current; } }, 30); + + newTabIdTimeoutIdRef.current = setTimeout(() => { + setNewTabId(null); + }, 100); }; const handleCloseTab = (event: React.MouseEvent | null, tabId: string) => { event?.stopPropagation(); services.WindowService.CloseTab(tabId); + tabsWrapperRef.current.style.setProperty("--tabs-wrapper-transition", "width 0.3s ease"); deleteLayoutStateAtomForTab(tabId); }; @@ -470,27 +478,32 @@ const TabBar = React.memo(({ workspace }: TabBarProps) => { }; const tabsWrapperWidth = tabIds.length * tabWidthRef.current; + console.log; return ( -
- +
+
-
- {tabIds.map((tabId, index) => ( - handleSelectTab(tabId)} - active={activetabid === tabId} - onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])} - onClose={(event) => handleCloseTab(event, tabId)} - onLoaded={() => handleTabLoaded(tabId)} - isBeforeActive={isBeforeActive(tabId)} - isDragging={draggingTab === tabId} - /> - ))} +
+ {tabIds.map((tabId, index) => { + return ( + handleSelectTab(tabId)} + active={activetabid === tabId} + onDragStart={(event) => handleDragStart(event, tabId, tabRefs.current[index])} + onClose={(event) => handleCloseTab(event, tabId)} + onLoaded={() => handleTabLoaded(tabId)} + isBeforeActive={isBeforeActive(tabId)} + isDragging={draggingTab === tabId} + tabWidth={tabWidthRef.current} + isNew={tabId === newTabId} + /> + ); + })}