44ac7f1252
浮层处理 Escape 后截断冒泡,避免画布全局快捷键顺带关闭整个生成面板。 补齐 Escape 不外泄、外部点击不抢焦点两条回归测试。 预设轨道锁定测试改为同时校验 CSS 规则本体,堵住只断言类名的空转。
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { useEffect } from 'react';
|
|
|
|
type FloatingOptionBoundaryRef = {
|
|
readonly current: HTMLElement | null;
|
|
};
|
|
|
|
type UseImageCanvasFloatingOptionDismissOptions = {
|
|
isOpen: boolean;
|
|
boundaryRefs: Array<FloatingOptionBoundaryRef | null | undefined>;
|
|
onDismiss: () => void;
|
|
/** Escape 关闭后把焦点还给的触发按钮;不传则只关闭不移焦。 */
|
|
restoreFocusRef?: FloatingOptionBoundaryRef | null;
|
|
};
|
|
|
|
function isEventInsideBoundary(
|
|
target: EventTarget | null,
|
|
boundaryRefs: Array<FloatingOptionBoundaryRef | null | undefined>,
|
|
) {
|
|
if (!(target instanceof Node)) {
|
|
return false;
|
|
}
|
|
|
|
return boundaryRefs.some((boundaryRef) => {
|
|
const element = boundaryRef?.current;
|
|
return element ? element.contains(target) : false;
|
|
});
|
|
}
|
|
|
|
function isEventInsideFloatingMenu(target: EventTarget | null) {
|
|
return (
|
|
target instanceof Element &&
|
|
target.closest('.image-canvas-editor__portal-menu')
|
|
);
|
|
}
|
|
|
|
export function useImageCanvasFloatingOptionDismiss({
|
|
isOpen,
|
|
boundaryRefs,
|
|
onDismiss,
|
|
restoreFocusRef,
|
|
}: UseImageCanvasFloatingOptionDismissOptions) {
|
|
useEffect(() => {
|
|
if (!isOpen || typeof document === 'undefined') {
|
|
return undefined;
|
|
}
|
|
|
|
const handleClick = (event: MouseEvent) => {
|
|
// 中文注释:选项项点击后要保留浮层;父级面板其它区域点击才收起。
|
|
if (
|
|
isEventInsideBoundary(event.target, boundaryRefs) ||
|
|
isEventInsideFloatingMenu(event.target)
|
|
) {
|
|
return;
|
|
}
|
|
onDismiss();
|
|
};
|
|
|
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
if (event.key !== 'Escape') {
|
|
return;
|
|
}
|
|
// 画布全局快捷键在 window 上监听同一个 Escape,且会把整个生成 dialog 关掉。
|
|
// 浮层打开时 Escape 归浮层所有:document 在冒泡路径上早于 window,这里截断,
|
|
// 否则用户只想收起参数浮层却会连整个面板一起丢失。
|
|
event.stopPropagation();
|
|
// 浮层 portal 到 body,关闭时焦点会掉到 body;只有焦点确实在浮层里才把它还给
|
|
// 触发按钮,避免抢走用户正在编辑的输入框。
|
|
const shouldRestoreFocus = isEventInsideFloatingMenu(
|
|
document.activeElement,
|
|
);
|
|
onDismiss();
|
|
if (shouldRestoreFocus) {
|
|
restoreFocusRef?.current?.focus();
|
|
}
|
|
};
|
|
|
|
document.addEventListener('click', handleClick);
|
|
document.addEventListener('keydown', handleKeyDown);
|
|
return () => {
|
|
document.removeEventListener('click', handleClick);
|
|
document.removeEventListener('keydown', handleKeyDown);
|
|
};
|
|
}, [boundaryRefs, isOpen, onDismiss, restoreFocusRef]);
|
|
}
|