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++

View File

@@ -450,6 +450,60 @@
"generateTitleFromImage": "Générer un titre à partir de l'image",
"titleGenerated": "Titre généré à partir de l'image"
},
"richTextEditor": {
"slashHint": "↑↓ naviguer · Entrée insérer · Tab changer de section",
"slashLoading": "IA Note réfléchit...",
"slashTabAll": "Tout",
"slashCatBasic": "Blocs de base",
"slashCatMedia": "Médias",
"slashCatFormatting": "Mise en forme",
"slashCatAi": "IA Note",
"insertImage": "Insérer une image",
"imageUrlPlaceholder": "https://exemple.com/image.png",
"preview": "Aperçu",
"cancel": "Annuler",
"insert": "Insérer",
"slashText": "Texte",
"slashTextDesc": "Paragraphe simple",
"slashH1": "Titre 1",
"slashH1Desc": "Grand titre de section",
"slashH2": "Titre 2",
"slashH2Desc": "Titre de section moyen",
"slashH3": "Titre 3",
"slashH3Desc": "Petit titre de section",
"slashBullet": "Liste à puces",
"slashBulletDesc": "Liste non ordonnée",
"slashNumbered": "Liste numérotée",
"slashNumberedDesc": "Liste ordonnée numérotée",
"slashTodo": "Liste de tâches",
"slashTodoDesc": "Cases à cocher pour les tâches",
"slashQuote": "Citation",
"slashQuoteDesc": "Capturer une citation",
"slashCode": "Bloc de code",
"slashCodeDesc": "Extrait de code",
"slashDivider": "Séparateur",
"slashDividerDesc": "Séparateur horizontal",
"slashImage": "Image",
"slashImageDesc": "Intégrer une image depuis une URL",
"slashAlignLeft": "Aligner à gauche",
"slashAlignLeftDesc": "Aligner le texte à gauche",
"slashAlignCenter": "Centrer",
"slashAlignCenterDesc": "Centrer le texte",
"slashAlignRight": "Aligner à droite",
"slashAlignRightDesc": "Aligner le texte à droite",
"slashSuperscript": "Exposant",
"slashSuperscriptDesc": "Texte au-dessus de la ligne de base",
"slashSubscript": "Indice",
"slashSubscriptDesc": "Texte sous la ligne de base",
"slashClarify": "Clarifier",
"slashClarifyDesc": "Rendre le texte plus clair",
"slashShorten": "Raccourcir",
"slashShortenDesc": "Condenser le texte",
"slashImprove": "Améliorer",
"slashImproveDesc": "Améliorer le style",
"slashExpand": "Développer",
"slashExpandDesc": "Élaborer et enrichir le texte"
},
"titleSuggestions": {
"available": "Suggestions de titre",
"title": "Suggestions IA",