diff --git a/src/shared/components/BlockEditor/BlockEditor.client.js b/src/shared/components/BlockEditor/BlockEditor.client.js index 09ccb4d..ba53c41 100644 --- a/src/shared/components/BlockEditor/BlockEditor.client.js +++ b/src/shared/components/BlockEditor/BlockEditor.client.js @@ -407,38 +407,8 @@ export default function BlockEditor({ commitChange(next, { immediate: true }); } - // --- Raccourcis globaux --- + // --- Raccourcis globaux (Undo/Redo seulement) --- function handleGlobalKeyDown(e) { - // Slash menu : navigation et sélection - if (slashState) { - const items = getSlashItems(slashState.query, enabledBlocks); - if (e.key === 'ArrowDown') { - e.preventDefault(); - setSlashState(s => ({ ...s, selectedIndex: Math.min((s.selectedIndex ?? 0) + 1, Math.max(items.length - 1, 0)) })); - return; - } - if (e.key === 'ArrowUp') { - e.preventDefault(); - setSlashState(s => ({ ...s, selectedIndex: Math.max((s.selectedIndex ?? 0) - 1, 0) })); - return; - } - if (e.key === 'Enter') { - if (items.length > 0) { - e.preventDefault(); - e.stopPropagation(); - const def = items[slashState.selectedIndex ?? 0]; - if (def) handleSlashSelect(def.type); - return; - } - } - if (e.key === 'Escape') { - e.preventDefault(); - setSlashState(null); - return; - } - } - - // Undo / Redo if ((e.ctrlKey || e.metaKey) && e.key === 'z') { e.preventDefault(); if (e.shiftKey) handleRedo(); @@ -452,6 +422,45 @@ export default function BlockEditor({ } } + // Le slash menu utilise un listener au niveau document en phase capture pour + // intercepter les touches avant que le contentEditable ne gère ses défauts + // (sinon ↑/↓ déplacent le caret et Entrée insère une nouvelle ligne). + useEffect(() => { + if (!slashState) return; + function onKey(e) { + if (!slashState) return; + const items = getSlashItems(slashState.query, enabledBlocks); + if (e.key === 'ArrowDown') { + e.preventDefault(); + e.stopPropagation(); + setSlashState(s => s && ({ + ...s, + selectedIndex: items.length === 0 ? 0 : ((s.selectedIndex ?? 0) + 1) % items.length, + })); + } else if (e.key === 'ArrowUp') { + e.preventDefault(); + e.stopPropagation(); + setSlashState(s => s && ({ + ...s, + selectedIndex: items.length === 0 ? 0 : ((s.selectedIndex ?? 0) - 1 + items.length) % items.length, + })); + } else if (e.key === 'Enter') { + if (items.length > 0) { + e.preventDefault(); + e.stopPropagation(); + const def = items[slashState.selectedIndex ?? 0]; + if (def) handleSlashSelect(def.type); + } + } else if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + setSlashState(null); + } + } + document.addEventListener('keydown', onKey, true); + return () => document.removeEventListener('keydown', onKey, true); + }, [slashState, enabledBlocks]); + return (
{label && ( diff --git a/src/shared/components/BlockEditor/SlashMenu.client.js b/src/shared/components/BlockEditor/SlashMenu.client.js index 1d6276f..2deb9cf 100644 --- a/src/shared/components/BlockEditor/SlashMenu.client.js +++ b/src/shared/components/BlockEditor/SlashMenu.client.js @@ -1,6 +1,10 @@ 'use client'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; + +const MENU_WIDTH = 256; // w-64 +const MENU_MAX_HEIGHT = 288; // max-h-72 +const VIEWPORT_MARGIN = 8; import { listBlocks } from './blockRegistry.js'; // Menu flottant des commandes. Affiché ancré à un élément (anchorRect). @@ -45,23 +49,51 @@ export default function SlashMenu({ }, [allowed, query]); const listRef = useRef(null); + const [position, setPosition] = useState({ top: 0, left: 0, maxHeight: MENU_MAX_HEIGHT }); - // Scroll l'élément sélectionné dans la vue + // Scroll l'élément sélectionné dans la vue (interne au menu uniquement) useEffect(() => { const el = listRef.current?.querySelector(`[data-slash-index="${selectedIndex}"]`); if (el) el.scrollIntoView({ block: 'nearest' }); }, [selectedIndex]); + // Positionnement adaptatif : flip au-dessus si pas assez de place en bas, + // clamp horizontalement, et limite la hauteur à l'espace disponible. + useLayoutEffect(() => { + if (!anchorRect || typeof window === 'undefined') return; + const vh = window.innerHeight; + const vw = window.innerWidth; + const spaceBelow = vh - anchorRect.bottom - VIEWPORT_MARGIN; + const spaceAbove = anchorRect.top - VIEWPORT_MARGIN; + + let top; + let maxHeight; + if (spaceBelow >= Math.min(MENU_MAX_HEIGHT, 200) || spaceBelow >= spaceAbove) { + top = anchorRect.bottom + 6; + maxHeight = Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceBelow - 6)); + } else { + maxHeight = Math.max(120, Math.min(MENU_MAX_HEIGHT, spaceAbove - 6)); + top = anchorRect.top - 6 - maxHeight; + } + + let left = anchorRect.left; + if (left + MENU_WIDTH + VIEWPORT_MARGIN > vw) { + left = Math.max(VIEWPORT_MARGIN, vw - MENU_WIDTH - VIEWPORT_MARGIN); + } + if (left < VIEWPORT_MARGIN) left = VIEWPORT_MARGIN; + + setPosition({ top, left, maxHeight }); + }, [anchorRect, items.length]); + if (!anchorRect) return null; - const top = anchorRect.bottom + 6; - const left = anchorRect.left; + const { top, left, maxHeight } = position; if (items.length === 0) { return (
Aucune commande pour « {query} »
@@ -71,8 +103,8 @@ export default function SlashMenu({ return (
e.preventDefault()} // ne pas voler le focus > {items.map((def, i) => {