@@ -54,207 +54,6 @@ interface DemoContextMenuClientProps {
5454 preloadedDataById : Readonly < Record < string , FileTreePreloadedData > > ;
5555}
5656
57- function LocalProjectHeader ( {
58- projectName,
59- onAddFile,
60- onAddFolder,
61- } : {
62- projectName : string ;
63- onAddFile : ( ) => void ;
64- onAddFolder : ( ) => void ;
65- } ) {
66- return (
67- < div className = "flex items-center justify-between gap-2 px-3 py-2" >
68- < div className = "min-w-0 truncate text-sm font-medium text-neutral-200" >
69- { projectName } /
70- </ div >
71- < div className = "flex items-center gap-3" >
72- < button
73- type = "button"
74- title = "New file"
75- onClick = { onAddFile }
76- className = "h-4 w-4 text-neutral-400 hover:text-neutral-100"
77- >
78- < IconFilePlus aria-hidden = "true" />
79- </ button >
80- < button
81- type = "button"
82- title = "New folder"
83- onClick = { onAddFolder }
84- className = "h-4 w-4 text-neutral-400 hover:text-neutral-100"
85- >
86- < IconFolderPlus aria-hidden = "true" />
87- </ button >
88- </ div >
89- </ div >
90- ) ;
91- }
92-
93- function getParentPath ( path : string ) : string {
94- const normalizedPath = path . endsWith ( '/' ) ? path . slice ( 0 , - 1 ) : path ;
95- const lastSlashIndex = normalizedPath . lastIndexOf ( '/' ) ;
96- return lastSlashIndex < 0
97- ? ''
98- : `${ normalizedPath . slice ( 0 , lastSlashIndex + 1 ) } ` ;
99- }
100-
101- function getUniquePath ( model : FileTreeModel , basePath : string ) : string {
102- let suffix = 0 ;
103- let candidate = basePath ;
104- while ( model . getItem ( candidate ) != null ) {
105- suffix += 1 ;
106- if ( basePath . endsWith ( '/' ) ) {
107- candidate = `${ basePath . slice ( 0 , - 1 ) } -${ String ( suffix ) } /` ;
108- continue ;
109- }
110-
111- const dotIndex = basePath . lastIndexOf ( '.' ) ;
112- const slashIndex = basePath . lastIndexOf ( '/' ) ;
113- if ( dotIndex > slashIndex ) {
114- candidate = `${ basePath . slice ( 0 , dotIndex ) } -${ String ( suffix ) } ${ basePath . slice ( dotIndex ) } ` ;
115- continue ;
116- }
117-
118- candidate = `${ basePath } -${ String ( suffix ) } ` ;
119- }
120- return candidate ;
121- }
122-
123- function ContextMenuContents ( {
124- context,
125- portalContainer,
126- onAddFile,
127- onAddFolder,
128- onDelete,
129- onRename,
130- } : {
131- context : Pick <
132- ContextMenuOpenContext ,
133- 'anchorRect' | 'close' | 'restoreFocus'
134- > ;
135- portalContainer ?: HTMLElement | null ;
136- onAddFile : ( ) => void ;
137- onAddFolder : ( ) => void ;
138- onDelete : ( ) => void ;
139- onRename : ( ) => void ;
140- } ) {
141- const closeAfter = ( action : ( ) => void ) => {
142- action ( ) ;
143- context . close ( ) ;
144- } ;
145-
146- return (
147- < DropdownMenu
148- open
149- modal = { false }
150- onOpenChange = { ( open ) => ! open && context . close ( ) }
151- >
152- < DropdownMenuTrigger asChild >
153- < button
154- type = "button"
155- aria-hidden = "true"
156- tabIndex = { - 1 }
157- style = { getFloatingContextMenuTriggerStyle ( context . anchorRect ) }
158- />
159- </ DropdownMenuTrigger >
160- < DropdownMenuContent
161- container = { portalContainer }
162- data-file-tree-context-menu-root = "true"
163- align = "center"
164- side = "bottom"
165- sideOffset = { 4 }
166- className = "min-w-[180px]"
167- onCloseAutoFocus = { ( event ) => {
168- event . preventDefault ( ) ;
169- context . restoreFocus ( ) ;
170- } }
171- >
172- < DropdownMenuItem
173- onSelect = { ( ) => {
174- closeAfter ( onAddFile ) ;
175- } }
176- >
177- New file
178- </ DropdownMenuItem >
179- < DropdownMenuItem
180- onSelect = { ( ) => {
181- closeAfter ( onAddFolder ) ;
182- } }
183- >
184- New folder
185- </ DropdownMenuItem >
186- < DropdownMenuItem
187- onSelect = { ( ) => {
188- context . close ( { restoreFocus : false } ) ;
189- onRename ( ) ;
190- } }
191- >
192- Rename
193- </ DropdownMenuItem >
194- < DropdownMenuSeparator />
195- < DropdownMenuItem
196- variant = "danger"
197- onSelect = { ( ) => {
198- closeAfter ( onDelete ) ;
199- } }
200- >
201- Delete
202- </ DropdownMenuItem >
203- </ DropdownMenuContent >
204- </ DropdownMenu >
205- ) ;
206- }
207-
208- function clearContextMenuSlot ( {
209- menuRootRef,
210- slotElement,
211- } : {
212- menuRootRef : { current : ReactDomRoot | null } ;
213- slotElement : HTMLDivElement ;
214- } ) : void {
215- const currentRoot = menuRootRef . current ;
216- if ( currentRoot == null ) {
217- return ;
218- }
219-
220- slotElement . style . display = 'none' ;
221- currentRoot . render ( null ) ;
222- }
223-
224- function useHeaderSlotRenderer (
225- modelRef : { current : FileTreeModel | null } ,
226- projectName : string
227- ) {
228- const slotElementRef = useRef < HTMLDivElement | null > ( null ) ;
229- const headerRootRef = useRef < ReactDomRoot | null > ( null ) ;
230-
231- return useCallback ( ( ) => {
232- const slotElement = slotElementRef . current ?? document . createElement ( 'div' ) ;
233- slotElementRef . current = slotElement ;
234- slotElement . style . display = 'block' ;
235- headerRootRef . current ??= createRoot ( slotElement ) ;
236-
237- const model = modelRef . current ;
238- if ( model == null ) {
239- return slotElement ;
240- }
241-
242- headerRootRef . current . render (
243- < LocalProjectHeader
244- projectName = { projectName }
245- onAddFile = { ( ) => {
246- model . add ( getUniquePath ( model , 'new-file.ts' ) ) ;
247- } }
248- onAddFolder = { ( ) => {
249- model . add ( getUniquePath ( model , 'new-folder/' ) ) ;
250- } }
251- />
252- ) ;
253-
254- return slotElement ;
255- } , [ modelRef , projectName ] ) ;
256- }
257-
25857function getProjectNameForMode ( mode : ContextMenuTriggerMode ) : string {
25958 switch ( mode ) {
26059 case 'button' :
0 commit comments