refactor(BlockEditor): replace hover-based submenu open/close with click-toggle
- remove timer-based submenu close logic (scheduleSubmenuClose, cancelSubmenuClose, submenuTimerRef) from BlockActionsMenu - replace onMouseEnter/onMouseLeave handlers with onClick toggle on submenu trigger - remove SUBMENU_CLOSE_DELAY constant and hover handlers from inline Toolbar submenus - update README to reflect click-to-open/close-on-outside-click behavior for all submenus
This commit is contained in:
@@ -27,7 +27,6 @@ import {
|
||||
const TOOLBAR_HEIGHT = 36;
|
||||
const TOOLBAR_GAP = 8;
|
||||
const VIEWPORT_MARGIN = 8;
|
||||
const SUBMENU_CLOSE_DELAY = 120;
|
||||
|
||||
const SIMPLE_BUTTONS = [
|
||||
{ type: 'bold', label: 'B', title: 'Gras (Ctrl+B)', className: 'font-bold' },
|
||||
@@ -41,7 +40,6 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
const ref = useRef(null);
|
||||
const [pos, setPos] = useState({ top: 0, left: 0, flipped: false });
|
||||
const [openSubmenu, setOpenSubmenu] = useState(null);
|
||||
const submenuTimerRef = useRef(null);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [linkNewTab, setLinkNewTab] = useState(false);
|
||||
|
||||
@@ -50,6 +48,25 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
return () => { onPinChange?.(false); };
|
||||
}, [openSubmenu, onPinChange]);
|
||||
|
||||
// Sous-menus en mode clic : fermeture sur clic extérieur ou Escape.
|
||||
useEffect(() => {
|
||||
if (openSubmenu === null) return;
|
||||
function handleDocMouseDown(e) {
|
||||
if (ref.current && !ref.current.contains(e.target)) {
|
||||
setOpenSubmenu(null);
|
||||
}
|
||||
}
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === 'Escape') setOpenSubmenu(null);
|
||||
}
|
||||
document.addEventListener('mousedown', handleDocMouseDown);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleDocMouseDown);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [openSubmenu]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!rect || typeof window === 'undefined') return;
|
||||
const el = ref.current;
|
||||
@@ -70,32 +87,19 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
setPos({ top, left, flipped: flipBelow });
|
||||
}, [rect]);
|
||||
|
||||
function scheduleClose() {
|
||||
if (submenuTimerRef.current) clearTimeout(submenuTimerRef.current);
|
||||
submenuTimerRef.current = setTimeout(() => setOpenSubmenu(null), SUBMENU_CLOSE_DELAY);
|
||||
}
|
||||
function cancelClose() {
|
||||
if (submenuTimerRef.current) {
|
||||
clearTimeout(submenuTimerRef.current);
|
||||
submenuTimerRef.current = null;
|
||||
}
|
||||
}
|
||||
useEffect(() => () => {
|
||||
if (submenuTimerRef.current) clearTimeout(submenuTimerRef.current);
|
||||
}, []);
|
||||
|
||||
function openSubmenuFor(key) {
|
||||
cancelClose();
|
||||
if (openSubmenu !== key && key === 'link') {
|
||||
const link = activeMarks.find(m => m.type === 'link');
|
||||
setLinkUrl(link?.href ?? '');
|
||||
setLinkNewTab(link ? !!link.newTab : false);
|
||||
}
|
||||
setOpenSubmenu(key);
|
||||
function toggleSubmenu(key) {
|
||||
setOpenSubmenu(prev => {
|
||||
if (prev === key) return null;
|
||||
if (key === 'link') {
|
||||
const link = activeMarks.find(m => m.type === 'link');
|
||||
setLinkUrl(link?.href ?? '');
|
||||
setLinkNewTab(link ? !!link.newTab : false);
|
||||
}
|
||||
return key;
|
||||
});
|
||||
}
|
||||
|
||||
function closeSubmenu() {
|
||||
cancelClose();
|
||||
setOpenSubmenu(null);
|
||||
}
|
||||
|
||||
@@ -160,10 +164,7 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
active={isActive('color')}
|
||||
open={openSubmenu === 'color'}
|
||||
flipUp={pos.flipped}
|
||||
onMouseEnter={() => openSubmenuFor('color')}
|
||||
onMouseLeave={scheduleClose}
|
||||
onPanelEnter={cancelClose}
|
||||
onPanelLeave={scheduleClose}
|
||||
onToggle={() => toggleSubmenu('color')}
|
||||
icon={<TextColorIcon width={16} height={16} />}
|
||||
>
|
||||
<ColorGrid
|
||||
@@ -179,10 +180,7 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
active={isActive('highlight')}
|
||||
open={openSubmenu === 'highlight'}
|
||||
flipUp={pos.flipped}
|
||||
onMouseEnter={() => openSubmenuFor('highlight')}
|
||||
onMouseLeave={scheduleClose}
|
||||
onPanelEnter={cancelClose}
|
||||
onPanelLeave={scheduleClose}
|
||||
onToggle={() => toggleSubmenu('highlight')}
|
||||
icon={<HighlighterIcon width={16} height={16} />}
|
||||
>
|
||||
<ColorGrid
|
||||
@@ -198,10 +196,7 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
active={isActive('link')}
|
||||
open={openSubmenu === 'link'}
|
||||
flipUp={pos.flipped}
|
||||
onMouseEnter={() => openSubmenuFor('link')}
|
||||
onMouseLeave={scheduleClose}
|
||||
onPanelEnter={cancelClose}
|
||||
onPanelLeave={scheduleClose}
|
||||
onToggle={() => toggleSubmenu('link')}
|
||||
icon={<Link02Icon width={16} height={16} />}
|
||||
>
|
||||
<LinkForm
|
||||
@@ -230,32 +225,25 @@ export default function InlineToolbar({ rect, activeMarks, onToggleMark, onSetMa
|
||||
);
|
||||
}
|
||||
|
||||
// Wrapper hover-to-open pour les boutons de la toolbar avec sous-menu. La
|
||||
// fenêtre de tolérance (120 ms) entre `mouseleave` du trigger et
|
||||
// `mouseenter` du panneau permet de traverser le gap visuel.
|
||||
function SubmenuTrigger({ title, active, open, flipUp, icon, children, onMouseEnter, onMouseLeave, onPanelEnter, onPanelLeave }) {
|
||||
// Wrapper click-to-open pour les boutons de la toolbar avec sous-menu. Le
|
||||
// panneau reste ouvert tant qu'on n'a pas cliqué à l'extérieur ou appuyé
|
||||
// sur Escape (logique gérée par le parent `InlineToolbar`).
|
||||
function SubmenuTrigger({ title, active, open, flipUp, icon, children, onToggle }) {
|
||||
const panelPosition = flipUp ? 'bottom-full mb-1' : 'top-full mt-1';
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
title={title}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={onToggle}
|
||||
className={`h-7 px-1.5 flex items-center gap-0.5 rounded-lg text-[13px] leading-none text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-white/5 hover:text-neutral-900 dark:hover:text-white transition-colors duration-[120ms] ease-out ${active || open ? ICON_BTN_ACTIVE_CLASS : ''}`}
|
||||
>
|
||||
{icon}
|
||||
<ArrowDown01Icon width={10} height={10} className="opacity-60" />
|
||||
</button>
|
||||
{open && (
|
||||
<div
|
||||
className={`absolute ${panelPosition} left-0 ${BOX_CLASS} p-1.5 z-50`}
|
||||
onMouseEnter={onPanelEnter}
|
||||
onMouseLeave={onPanelLeave}
|
||||
>
|
||||
<div className={`absolute ${panelPosition} left-0 ${BOX_CLASS} p-1.5 z-50`}>
|
||||
{children}
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user