Files
FreeCADWasmDev/web/src/toolbar-visibility-menu.tsx
wangdequan 789e18ca3e
Some checks failed
FreeCAD master CI / Prepare (push) Has been cancelled
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
FreeCAD master CI / Pixi (push) Has been cancelled
FreeCAD master CI / Ubuntu (push) Has been cancelled
FreeCAD master CI / Windows (push) Has been cancelled
FreeCAD master CI / Lint (push) Has been cancelled
FreeCAD master CI / WrapUp (push) Has been cancelled
Stale Issues / stale (push) Has been cancelled
CodeQL Advanced (c-cpp) / Analyze (c-cpp) (push) Has been cancelled
Push Crowdin Translations / update-crowdin (push) Has been cancelled
Fetch Crowdin Translations / update-crowdin (push) Has been cancelled
Build Release / upload_src (push) Has been cancelled
Build Release / build (10.13, macos-15-intel, osx-64) (push) Has been cancelled
Build Release / build (11.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (15.0, macos-latest, osx-arm64) (push) Has been cancelled
Build Release / build (ubuntu-22.04, linux-64) (push) Has been cancelled
Build Release / build (ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Build Release / build (windows-latest, win-64) (push) Has been cancelled
Monthly issue metrics / issue metrics (push) Has been cancelled
Continue FreeCAD Web UI parity repairs
2026-09-23 14:03:56 +08:00

85 lines
4.3 KiB
TypeScript

import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
/** Desktop toolbar toggle actions, shared by the toolbar button and dock context menus. */
export function ToolbarVisibilityMenu({ workspace, names, visibility, disabled, toggle }: {
workspace: React.RefObject<HTMLDivElement | null>; names: string[]; visibility: Record<string, boolean>;
disabled: boolean; toggle: (name: string, visible: boolean) => void;
}) {
const [anchor, setAnchor] = useState<{ x: number; y: number } | null>(null);
const trigger = useRef<HTMLButtonElement>(null);
const menu = useRef<HTMLDivElement>(null);
const previousFocus = useRef<HTMLElement | null>(null);
const close = (restore = true) => {
setAnchor(null);
if (restore) {
const previous = previousFocus.current;
if (previous?.isConnected && previous.getClientRects().length) previous.focus();
else trigger.current?.focus();
}
};
const open = (x: number, y: number) => {
previousFocus.current = document.activeElement instanceof HTMLElement ? document.activeElement : trigger.current;
setAnchor({ x, y });
};
useEffect(() => {
const root = workspace.current;
if (!root) return;
const context = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (!target.closest('.toolbar-dock, .toolbar-placement-controls')) return;
event.preventDefault();
if (disabled) return;
const rect = target.getBoundingClientRect();
open(event.clientX || rect.left, event.clientY || rect.bottom);
};
root.addEventListener('contextmenu', context);
return () => root.removeEventListener('contextmenu', context);
}, [workspace, disabled]);
useLayoutEffect(() => {
if (!anchor || !menu.current) return;
const element = menu.current;
element.style.left = `${Math.max(4, Math.min(anchor.x, window.innerWidth - element.offsetWidth - 4))}px`;
element.style.top = `${Math.max(4, Math.min(anchor.y, window.innerHeight - element.offsetHeight - 4))}px`;
element.querySelector<HTMLButtonElement>('button')?.focus();
}, [anchor]);
useEffect(() => {
if (!anchor) return;
const dismiss = (event: PointerEvent) => {
if (!menu.current?.contains(event.target as Node) && !trigger.current?.contains(event.target as Node)) close(false);
};
const resize = () => close();
document.addEventListener('pointerdown', dismiss);
window.addEventListener('resize', resize);
return () => { document.removeEventListener('pointerdown', dismiss); window.removeEventListener('resize', resize); };
}, [anchor]);
useEffect(() => { if (disabled) setAnchor(null); }, [disabled]);
return <>
<button ref={trigger} type="button" disabled={disabled} aria-haspopup="menu" aria-expanded={!!anchor}
onClick={() => {
if (anchor) { close(); return; }
const rect = trigger.current!.getBoundingClientRect(); open(rect.left, rect.bottom);
}}>Toolbars</button>
{anchor && createPortal(<div ref={menu} role="menu" aria-label="Toolbars" className="toolbar-visibility-menu"
style={{ position: 'fixed', left: anchor.x, top: anchor.y, zIndex: 1000 }} onKeyDown={event => {
if (event.key === 'Escape' || event.key === 'Tab') {
if (event.key === 'Escape') event.preventDefault();
event.stopPropagation(); close(); return;
}
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
event.preventDefault(); event.stopPropagation();
const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('button')];
if (!items.length) return;
const index = items.indexOf(document.activeElement as HTMLButtonElement);
const next = event.key === 'Home' ? 0 : event.key === 'End' ? items.length - 1
: (index + (event.key === 'ArrowUp' ? -1 : 1) + items.length) % items.length;
items[next]?.focus();
}}>
{names.map(name => <button key={name} type="button" role="menuitemcheckbox" aria-checked={visibility[name] !== false}
title="Toggles this toolbar" onClick={() => { close(); toggle(name, visibility[name] === false); }}>
<span aria-hidden="true">{visibility[name] !== false ? '✓' : ''}</span>{name}
</button>)}
</div>, document.body)}
</>;
}