diff --git a/src/hub/links/hub.test.ts b/src/hub/links/hub.test.ts index 405d7d0..31b6dba 100644 --- a/src/hub/links/hub.test.ts +++ b/src/hub/links/hub.test.ts @@ -42,7 +42,11 @@ const sleep = (ms: number): Promise => new Promise((resolve) => setTimeout /** Installs `body` as the SSH program; `$D` is the test directory, `$@` the SSH arguments. */ const fakeSsh = (body: string): void => { const program = join(dir, 'ssh'); - writeFileSync(program, `#!/bin/sh\nD='${dir}'\nfor a in "$@"; do printf '%s\\0' "$a"; done > "$D/args"\necho $$ >> "$D/pids"\n${body}\n`); + // `args` appears whole, through a rename, so a reader never sees part of it. + writeFileSync( + program, + `#!/bin/sh\nD='${dir}'\nfor a in "$@"; do printf '%s\\0' "$a"; done > "$D/args.tmp"\nmv "$D/args.tmp" "$D/args"\necho $$ >> "$D/pids"\n${body}\n`, + ); chmodSync(program, 0o755); vi.stubEnv('WTD_SSH', program); }; diff --git a/src/web/ui/app.tsx b/src/web/ui/app.tsx index 2dfad91..453c6d4 100644 --- a/src/web/ui/app.tsx +++ b/src/web/ui/app.tsx @@ -116,6 +116,17 @@ const Hubs = ({ client }: { client: HubClient }) => { const [open, setOpen] = useState(false); const stateRef = useRef(state); const menuButton = useRef(null); + const menu = useRef(null); + useEffect(() => { + if (!open) return undefined; + const outside = (event: PointerEvent): void => { + if (event.target instanceof Node && menu.current?.contains(event.target) !== true) setOpen(false); + }; + document.addEventListener('pointerdown', outside, true); + return () => { + document.removeEventListener('pointerdown', outside, true); + }; + }, [open]); const hosts = client.store.hosts.value; const dispatch = (action: HubAction): void => { const { state: next, effect } = hubStep(stateRef.current, action); @@ -174,7 +185,7 @@ const Hubs = ({ client }: { client: HubClient }) => { /> )} {entries !== null && ( - +