fix: slash menu tabs stay open (menuRef.contains check) + full i18n support
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 44s

This commit is contained in:
2026-05-02 22:14:57 +02:00
parent 8ef67dcc1f
commit e076601275
2 changed files with 99 additions and 8 deletions

View File

@@ -1,6 +1,7 @@
'use client'
import { useEffect, useRef, useState, useCallback, forwardRef, useImperativeHandle } from 'react'
import { useLanguage } from '@/lib/i18n'
import { useEditor, EditorContent } from '@tiptap/react'
import { BubbleMenu } from '@tiptap/react/menus'
import StarterKit from '@tiptap/starter-kit'
@@ -323,6 +324,7 @@ function BubbleToolbar({ editor }: { editor: Editor | null }) {
}
function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertImage: (editor: Editor) => void }) {
const { t } = useLanguage()
const [isOpen, setIsOpen] = useState(false)
const [query, setQuery] = useState('')
const [selectedIndex, setSelectedIndex] = useState(0)
@@ -332,6 +334,38 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
const menuRef = useRef<HTMLDivElement>(null)
const selectedItemRef = useRef<HTMLButtonElement>(null)
// Translated category names (keys match slashCommands category field)
const CAT_LABELS: Record<string, string> = {
'Basic blocks': t('richTextEditor.slashCatBasic'),
'Media': t('richTextEditor.slashCatMedia'),
'Formatting': t('richTextEditor.slashCatFormatting'),
'IA Note': t('richTextEditor.slashCatAi'),
}
// Translated command list (keeps same order/icons/shortcuts as global slashCommands)
const localCommands: SlashItem[] = [
{ ...slashCommands[0], title: t('richTextEditor.slashText'), description: t('richTextEditor.slashTextDesc'), category: 'Basic blocks' },
{ ...slashCommands[1], title: t('richTextEditor.slashH1'), description: t('richTextEditor.slashH1Desc'), category: 'Basic blocks' },
{ ...slashCommands[2], title: t('richTextEditor.slashH2'), description: t('richTextEditor.slashH2Desc'), category: 'Basic blocks' },
{ ...slashCommands[3], title: t('richTextEditor.slashH3'), description: t('richTextEditor.slashH3Desc'), category: 'Basic blocks' },
{ ...slashCommands[4], title: t('richTextEditor.slashBullet'), description: t('richTextEditor.slashBulletDesc'), category: 'Basic blocks' },
{ ...slashCommands[5], title: t('richTextEditor.slashNumbered'), description: t('richTextEditor.slashNumberedDesc'), category: 'Basic blocks' },
{ ...slashCommands[6], title: t('richTextEditor.slashTodo'), description: t('richTextEditor.slashTodoDesc'), category: 'Basic blocks' },
{ ...slashCommands[7], title: t('richTextEditor.slashQuote'), description: t('richTextEditor.slashQuoteDesc'), category: 'Basic blocks' },
{ ...slashCommands[8], title: t('richTextEditor.slashCode'), description: t('richTextEditor.slashCodeDesc'), category: 'Basic blocks' },
{ ...slashCommands[9], title: t('richTextEditor.slashDivider'), description: t('richTextEditor.slashDividerDesc'), category: 'Basic blocks' },
{ ...slashCommands[10], title: t('richTextEditor.slashImage'), description: t('richTextEditor.slashImageDesc'), category: 'Media' },
{ ...slashCommands[11], title: t('richTextEditor.slashAlignLeft'), description: t('richTextEditor.slashAlignLeftDesc'), category: 'Formatting' },
{ ...slashCommands[12], title: t('richTextEditor.slashAlignCenter'), description: t('richTextEditor.slashAlignCenterDesc'), category: 'Formatting' },
{ ...slashCommands[13], title: t('richTextEditor.slashAlignRight'), description: t('richTextEditor.slashAlignRightDesc'), category: 'Formatting' },
{ ...slashCommands[14], title: t('richTextEditor.slashSuperscript'), description: t('richTextEditor.slashSuperscriptDesc'), category: 'Formatting' },
{ ...slashCommands[15], title: t('richTextEditor.slashSubscript'), description: t('richTextEditor.slashSubscriptDesc'), category: 'Formatting' },
{ ...slashCommands[16], title: t('richTextEditor.slashClarify'), description: t('richTextEditor.slashClarifyDesc'), category: 'IA Note' },
{ ...slashCommands[17], title: t('richTextEditor.slashShorten'), description: t('richTextEditor.slashShortenDesc'), category: 'IA Note' },
{ ...slashCommands[18], title: t('richTextEditor.slashImprove'), description: t('richTextEditor.slashImproveDesc'), category: 'IA Note' },
{ ...slashCommands[19], title: t('richTextEditor.slashExpand'), description: t('richTextEditor.slashExpandDesc'), category: 'IA Note' },
]
const closeMenu = useCallback(() => {
setIsOpen(false); setQuery(''); setSelectedIndex(0); setActiveCategory(null)
}, [])
@@ -364,10 +398,9 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
}, [editor, closeMenu, deleteSlashText, onInsertImage])
// All category names in order
const allCategories = Array.from(new Set(slashCommands.map(c => c.category || 'Basic blocks')))
const allCategories = Array.from(new Set(localCommands.map(c => c.category || 'Basic blocks')))
// Filtered by text query, then optionally by active tab
const textFiltered = slashCommands.filter(c => c.title.toLowerCase().includes(query.toLowerCase()) || c.description.toLowerCase().includes(query.toLowerCase()))
const textFiltered = localCommands.filter(c => c.title.toLowerCase().includes(query.toLowerCase()) || c.description.toLowerCase().includes(query.toLowerCase()))
const filtered = activeCategory ? textFiltered.filter(c => (c.category || 'Basic blocks') === activeCategory) : textFiltered
const categories = filtered.reduce((acc, item) => {
@@ -413,7 +446,11 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
}, [isOpen, editor, query])
useEffect(() => {
const handleClick = () => { if (isOpen) closeMenu() }
const handleClick = (e: MouseEvent) => {
if (isOpen && menuRef.current && !menuRef.current.contains(e.target as Node)) {
closeMenu()
}
}
document.addEventListener('click', handleClick)
return () => document.removeEventListener('click', handleClick)
}, [isOpen, closeMenu])
@@ -447,7 +484,7 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
onMouseDown={(e) => e.preventDefault()}
onClick={() => { setActiveCategory(null); setSelectedIndex(0) }}
>
Tout
{t('richTextEditor.slashTabAll')}
</button>
{allCategories.filter(cat => categories[cat]).map(cat => (
<button
@@ -466,20 +503,20 @@ function SlashCommandMenu({ editor, onInsertImage }: { editor: Editor; onInsertI
{/* Header hint */}
<div className="notion-slash-header">
<span> naviguer · Entrée insérer · Tab changer de section</span>
<span>{t('richTextEditor.slashHint')}</span>
</div>
{aiLoading && (
<div className="notion-slash-loading">
<Sparkles className="w-3.5 h-3.5 animate-spin" />
<span>IA Note réfléchit...</span>
<span>{t('richTextEditor.slashLoading')}</span>
</div>
)}
{!aiLoading && Object.entries(categories).map(([cat, items]) => (
<div key={cat} className="notion-slash-section">
<div className={cn('notion-slash-label', cat === 'IA Note' && 'notion-slash-label-ai')}>
{cat === 'IA Note' && <Sparkles className="w-3 h-3 inline mr-1" />}
{cat}
{CAT_LABELS[cat] || cat}
</div>
{items.map((item) => {
flatIndex++