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
85 lines
4.3 KiB
TypeScript
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)}
|
|
</>;
|
|
}
|