1- import React , { useEffect , useRef , useState } from 'react' ;
1+ import React , { useCallback , useEffect , useRef , useState } from 'react' ;
22import type { DocType , DocumentOrFolder } from '../types' ;
33import Button from './Button' ;
44import { FolderIcon , FileIcon , InfoIcon , PlusIcon , FolderPlusIcon , FolderDownIcon , PencilIcon , SearchIcon , XIcon , CopyIcon } from './Icons' ;
5+ import EmojiPickerOverlay from './EmojiPickerOverlay' ;
56
67export interface DocTypeCount {
78 type : DocType ;
@@ -126,12 +127,15 @@ const FolderOverview: React.FC<FolderOverviewProps> = ({
126127 const hasLanguageSummary = languageCounts . some ( ( { count } ) => count > 0 ) ;
127128 const fileInputRef = useRef < HTMLInputElement | null > ( null ) ;
128129 const titleInputRef = useRef < HTMLInputElement | null > ( null ) ;
130+ const titleSelectionRef = useRef < { start : number ; end : number } | null > ( null ) ;
129131
130132 const normalizedTitle = folder . title ?. trim ( ) ?? '' ;
131133 const displayTitle = normalizedTitle . length > 0 ? normalizedTitle : 'Untitled Folder' ;
132134
133135 const [ isEditingTitle , setIsEditingTitle ] = useState ( false ) ;
134136 const [ titleDraft , setTitleDraft ] = useState ( displayTitle ) ;
137+ const [ isTitleEmojiPickerOpen , setIsTitleEmojiPickerOpen ] = useState ( false ) ;
138+ const [ titleEmojiAnchor , setTitleEmojiAnchor ] = useState < { x : number ; y : number } | null > ( null ) ;
135139
136140 useEffect ( ( ) => {
137141 setTitleDraft ( displayTitle ) ;
@@ -143,8 +147,17 @@ const FolderOverview: React.FC<FolderOverviewProps> = ({
143147 requestAnimationFrame ( ( ) => {
144148 titleInputRef . current ?. focus ( ) ;
145149 titleInputRef . current ?. select ( ) ;
150+ const value = titleInputRef . current ?. value ?? titleDraft ;
151+ titleSelectionRef . current = { start : 0 , end : value . length } ;
146152 } ) ;
147153 }
154+ } , [ isEditingTitle , titleDraft ] ) ;
155+
156+ useEffect ( ( ) => {
157+ if ( ! isEditingTitle ) {
158+ setIsTitleEmojiPickerOpen ( false ) ;
159+ setTitleEmojiAnchor ( null ) ;
160+ }
148161 } , [ isEditingTitle ] ) ;
149162
150163 const handleImportClick = ( ) => {
@@ -175,6 +188,71 @@ const FolderOverview: React.FC<FolderOverviewProps> = ({
175188 setTitleDraft ( event . target . value ) ;
176189 } ;
177190
191+ const updateTitleSelection = useCallback ( ( ) => {
192+ const input = titleInputRef . current ;
193+ if ( ! input ) {
194+ return ;
195+ }
196+ const start = input . selectionStart ?? input . value . length ;
197+ const end = input . selectionEnd ?? input . value . length ;
198+ titleSelectionRef . current = { start, end } ;
199+ } , [ ] ) ;
200+
201+ const closeTitleEmojiPicker = useCallback ( ( ) => {
202+ setIsTitleEmojiPickerOpen ( false ) ;
203+ setTitleEmojiAnchor ( null ) ;
204+ if ( ! isEditingTitle ) {
205+ return ;
206+ }
207+ requestAnimationFrame ( ( ) => {
208+ const input = titleInputRef . current ;
209+ const selection = titleSelectionRef . current ;
210+ if ( input ) {
211+ input . focus ( ) ;
212+ if ( selection ) {
213+ input . setSelectionRange ( selection . start , selection . end ) ;
214+ }
215+ }
216+ } ) ;
217+ } , [ isEditingTitle ] ) ;
218+
219+ const handleTitleEmojiSelect = useCallback ( ( emoji : string ) => {
220+ const input = titleInputRef . current ;
221+ let selection = titleSelectionRef . current ;
222+
223+ if ( ! selection ) {
224+ if ( input ) {
225+ selection = {
226+ start : input . selectionStart ?? input . value . length ,
227+ end : input . selectionEnd ?? input . value . length ,
228+ } ;
229+ } else {
230+ const fallback = titleDraft . length ;
231+ selection = { start : fallback , end : fallback } ;
232+ }
233+ }
234+
235+ const { start, end } = selection ;
236+
237+ setTitleDraft ( ( previous ) => {
238+ const before = previous . slice ( 0 , start ) ;
239+ const after = previous . slice ( end ) ;
240+ return `${ before } ${ emoji } ${ after } ` ;
241+ } ) ;
242+
243+ const caretPosition = start + emoji . length ;
244+ titleSelectionRef . current = { start : caretPosition , end : caretPosition } ;
245+ closeTitleEmojiPicker ( ) ;
246+ } , [ closeTitleEmojiPicker , titleDraft . length ] ) ;
247+
248+ const handleTitleContextMenu = useCallback ( ( event : React . MouseEvent < HTMLInputElement > ) => {
249+ event . preventDefault ( ) ;
250+ event . stopPropagation ( ) ;
251+ updateTitleSelection ( ) ;
252+ setTitleEmojiAnchor ( { x : event . clientX , y : event . clientY } ) ;
253+ setIsTitleEmojiPickerOpen ( true ) ;
254+ } , [ updateTitleSelection ] ) ;
255+
178256 const handleTitleCancel = ( ) => {
179257 setTitleDraft ( displayTitle ) ;
180258 setIsEditingTitle ( false ) ;
@@ -193,6 +271,9 @@ const FolderOverview: React.FC<FolderOverviewProps> = ({
193271 } ;
194272
195273 const handleTitleBlur : React . FocusEventHandler < HTMLInputElement > = ( ) => {
274+ if ( isTitleEmojiPickerOpen ) {
275+ return ;
276+ }
196277 commitTitleChange ( ) ;
197278 } ;
198279
@@ -225,6 +306,10 @@ const FolderOverview: React.FC<FolderOverviewProps> = ({
225306 onChange = { handleTitleChange }
226307 onBlur = { handleTitleBlur }
227308 onKeyDown = { handleTitleKeyDown }
309+ onContextMenu = { handleTitleContextMenu }
310+ onSelect = { updateTitleSelection }
311+ onKeyUp = { updateTitleSelection }
312+ onMouseUp = { updateTitleSelection }
228313 className = "max-w-full rounded-sm border border-border-color bg-transparent px-1 py-1 text-xl font-semibold leading-tight text-text-main focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
229314 aria-label = "Edit folder name"
230315 />
@@ -548,6 +633,13 @@ const FolderOverview: React.FC<FolderOverviewProps> = ({
548633 </ section >
549634 </ div >
550635 </ div >
636+ < EmojiPickerOverlay
637+ isOpen = { isEditingTitle && isTitleEmojiPickerOpen }
638+ anchor = { titleEmojiAnchor }
639+ onClose = { closeTitleEmojiPicker }
640+ onSelectEmoji = { handleTitleEmojiSelect }
641+ ariaLabel = "Insert emoji into folder name"
642+ />
551643 </ div >
552644 ) ;
553645} ;
0 commit comments