1- import React , { useState , useRef , useEffect } from 'react' ;
1+ import React , { useState , useRef , useEffect , useLayoutEffect } from 'react' ;
22// Fix: Correctly import the DocumentOrFolder type.
33import type { DocumentOrFolder , DraggedNodeTransfer } from '../types' ;
44import IconButton from './IconButton' ;
55import { FileIcon , FolderIcon , FolderOpenIcon , TrashIcon , ChevronRightIcon , ChevronDownIcon , CopyIcon , ArrowUpIcon , ArrowDownIcon , CodeIcon , SaveIcon , LockClosedIcon , LockOpenIcon } from './Icons' ;
6+ import Tooltip from './Tooltip' ;
67
78export interface DocumentNode extends DocumentOrFolder {
89 children : DocumentNode [ ] ;
@@ -139,10 +140,12 @@ const DocumentTreeItem: React.FC<DocumentTreeItemProps> = (props) => {
139140 const [ dropPosition , setDropPosition ] = useState < 'before' | 'after' | 'inside' | null > ( null ) ;
140141 const [ isHovered , setIsHovered ] = useState ( false ) ;
141142 const [ lockedRowHeight , setLockedRowHeight ] = useState < number | null > ( null ) ;
143+ const [ isTitleTruncated , setIsTitleTruncated ] = useState ( false ) ;
142144
143145 const renameInputRef = useRef < HTMLInputElement > ( null ) ;
144146 const itemRef = useRef < HTMLLIElement > ( null ) ;
145147 const rowRef = useRef < HTMLDivElement > ( null ) ;
148+ const titleRef = useRef < HTMLSpanElement > ( null ) ;
146149
147150 const isSelected = selectedIds . has ( node . id ) ;
148151 const isFocused = focusedItemId === node . id ;
@@ -190,6 +193,51 @@ const DocumentTreeItem: React.FC<DocumentTreeItemProps> = (props) => {
190193 }
191194 } , [ isSelected , isRenaming ] ) ;
192195
196+ useLayoutEffect ( ( ) => {
197+ if ( ! isHovered || ! areActionsVisible || isRenaming ) {
198+ setIsTitleTruncated ( false ) ;
199+ return ;
200+ }
201+
202+ const titleElement = titleRef . current ;
203+
204+ if ( ! titleElement ) {
205+ setIsTitleTruncated ( false ) ;
206+ return ;
207+ }
208+
209+ const checkTruncation = ( ) => {
210+ const truncated = titleElement . scrollWidth > titleElement . clientWidth + 0.5 ;
211+ setIsTitleTruncated ( truncated ) ;
212+ } ;
213+
214+ checkTruncation ( ) ;
215+
216+ if ( typeof window === 'undefined' ) {
217+ return ;
218+ }
219+
220+ let frame : number | null = window . requestAnimationFrame ( checkTruncation ) ;
221+
222+ let resizeObserver : ResizeObserver | null = null ;
223+ if ( typeof ResizeObserver !== 'undefined' ) {
224+ resizeObserver = new ResizeObserver ( checkTruncation ) ;
225+ resizeObserver . observe ( titleElement ) ;
226+ }
227+
228+ window . addEventListener ( 'resize' , checkTruncation ) ;
229+
230+ return ( ) => {
231+ if ( frame !== null ) {
232+ window . cancelAnimationFrame ( frame ) ;
233+ }
234+ window . removeEventListener ( 'resize' , checkTruncation ) ;
235+ if ( resizeObserver ) {
236+ resizeObserver . disconnect ( ) ;
237+ }
238+ } ;
239+ } , [ areActionsVisible , displayTitle , isHovered , isRenaming , searchTerm ] ) ;
240+
193241 const handleRenameStart = ( e : React . MouseEvent ) => {
194242 e . stopPropagation ( ) ;
195243 setIsRenaming ( true ) ;
@@ -381,6 +429,7 @@ const DocumentTreeItem: React.FC<DocumentTreeItemProps> = (props) => {
381429 />
382430 ) : (
383431 < span
432+ ref = { titleRef }
384433 className = { `flex-1 px-1 ${
385434 areActionsVisible ? 'truncate' : 'whitespace-normal break-words'
386435 } `}
@@ -390,6 +439,20 @@ const DocumentTreeItem: React.FC<DocumentTreeItemProps> = (props) => {
390439 ) }
391440 </ div >
392441
442+ { isHovered && isTitleTruncated && titleRef . current && (
443+ < Tooltip
444+ targetRef = { titleRef }
445+ content = { (
446+ < span className = "inline-flex max-w-xs whitespace-pre-wrap break-words text-left leading-snug gap-1" >
447+ { emojiForNode && ! isFolder && (
448+ < span aria-hidden = "true" > { emojiForNode } </ span >
449+ ) }
450+ < span > { highlightMatches ( displayTitle , searchTerm ) } </ span >
451+ </ span >
452+ ) }
453+ />
454+ ) }
455+
393456 { ! isRenaming && (
394457 < div
395458 className = { `transition-opacity flex items-center ${
0 commit comments