fix(BlockEditor): close slash menu when clicking outside or on different block
- add `handleFocus` in Block to reopen slash menu on focus if content starts with `/` - add `outerRef` on editor root div to detect outside clicks - add `useEffect` with document `mousedown` listener to close slash menu when clicking outside the editor or on a different block - add `data-slash-menu` attribute on SlashMenu to exclude it from the close trigger
This commit is contained in:
@@ -205,6 +205,17 @@ const Block = forwardRef(function Block(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleFocus() {
|
||||||
|
if (def?.isText && !disabled) {
|
||||||
|
const el = editableRef.current;
|
||||||
|
const text = el?.textContent ?? '';
|
||||||
|
if (text.startsWith('/') && !text.slice(1).includes(' ')) {
|
||||||
|
onSlashOpen?.({ blockId: block.id, query: text.slice(1) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onFocus?.(block.id);
|
||||||
|
}
|
||||||
|
|
||||||
function handlePaste(e) {
|
function handlePaste(e) {
|
||||||
// MVP : on colle uniquement du texte brut pour éviter le HTML externe.
|
// MVP : on colle uniquement du texte brut pour éviter le HTML externe.
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -324,7 +335,7 @@ const Block = forwardRef(function Block(
|
|||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onFocus={() => onFocus?.(block.id)}
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -336,7 +347,7 @@ const Block = forwardRef(function Block(
|
|||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
onFocus={() => onFocus?.(block.id)}
|
onFocus={handleFocus}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
) : def.Component ? (
|
) : def.Component ? (
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export default function BlockEditor({
|
|||||||
const blocks = useMemo(() => ensureNonEmpty(value), [value]);
|
const blocks = useMemo(() => ensureNonEmpty(value), [value]);
|
||||||
const blockRefs = useRef(new Map());
|
const blockRefs = useRef(new Map());
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
|
const outerRef = useRef(null);
|
||||||
const [focusBlockId, setFocusBlockId] = useState(null);
|
const [focusBlockId, setFocusBlockId] = useState(null);
|
||||||
const [focusOffset, setFocusOffset] = useState(null);
|
const [focusOffset, setFocusOffset] = useState(null);
|
||||||
const [selectedBlockIds, setSelectedBlockIds] = useState(() => new Set());
|
const [selectedBlockIds, setSelectedBlockIds] = useState(() => new Set());
|
||||||
@@ -764,6 +765,26 @@ export default function BlockEditor({
|
|||||||
selectAllBlocks();
|
selectAllBlocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ferme le slash menu quand on clique en dehors de l'éditeur ou sur un
|
||||||
|
// bloc différent de celui qui a ouvert le menu.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!slashState) return;
|
||||||
|
function onDocMouseDown(e) {
|
||||||
|
if (e.target instanceof Element && e.target.closest('[data-slash-menu]')) return;
|
||||||
|
const outer = outerRef.current;
|
||||||
|
if (!outer || !outer.contains(e.target)) {
|
||||||
|
setSlashState(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const blockEl = e.target instanceof Element ? e.target.closest('[data-block-id]') : null;
|
||||||
|
if (!blockEl || blockEl.getAttribute('data-block-id') !== slashState.blockId) {
|
||||||
|
setSlashState(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('mousedown', onDocMouseDown, true);
|
||||||
|
return () => document.removeEventListener('mousedown', onDocMouseDown, true);
|
||||||
|
}, [slashState]);
|
||||||
|
|
||||||
// Le slash menu utilise un listener au niveau document en phase capture pour
|
// 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
|
// 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).
|
// (sinon ↑/↓ déplacent le caret et Entrée insère une nouvelle ligne).
|
||||||
@@ -804,7 +825,7 @@ export default function BlockEditor({
|
|||||||
}, [slashState, enabledBlocks]);
|
}, [slashState, enabledBlocks]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div ref={outerRef} className="space-y-2">
|
||||||
{label && (
|
{label && (
|
||||||
<label className="block text-xs font-medium text-neutral-700 dark:text-neutral-400">
|
<label className="block text-xs font-medium text-neutral-700 dark:text-neutral-400">
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export default function SlashMenu({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
|
data-slash-menu
|
||||||
className="fixed z-50 overflow-y-auto rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md py-1"
|
className="fixed z-50 overflow-y-auto rounded-lg border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md py-1"
|
||||||
style={{ top, left, width: MENU_WIDTH, maxHeight }}
|
style={{ top, left, width: MENU_WIDTH, maxHeight }}
|
||||||
onMouseDown={(e) => e.preventDefault()} // ne pas voler le focus
|
onMouseDown={(e) => e.preventDefault()} // ne pas voler le focus
|
||||||
|
|||||||
Reference in New Issue
Block a user