Explorar o código

refactor(client): share the outside-pointer dismissal hook

The jobs list and the Cordis panel carried identical outside-pointerdown
close effects, which the duplication gate rejects; both now use
useDismissOnOutsidePointer from ui-primitives.

Refs #2526
Yif hai 2 meses
pai
achega
b5deda1f05

+ 2 - 11
packages/client/ui-jobs/src/client/JobListAction.tsx

@@ -1,6 +1,6 @@
 import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
 import type { JobView } from '@deepseek-ai/dsh-client-runtime/client'
-import { IconChevronDownOutline14, StateDot, type StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
+import { IconChevronDownOutline14, StateDot, useDismissOnOutsidePointer, type StateDotState } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { PropsLocale, PropsRuntime, TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
 import { NS } from './locales.ts'
 import type {} from '@deepseek-ai/dsh-client-ui-conversation/client'
@@ -101,16 +101,7 @@ export function JobListAction({ sessionId, useSessions, t }: JobListActionProps)
   const rows = useMemo(() => ordered(jobs), [jobs])
   const liveCount = useMemo(() => jobs.filter(isLive).length, [jobs])
 
-  useEffect(() => {
-    if (!open) return
-    const closeOutside = (event: PointerEvent): void => {
-      if (event.target instanceof Node && !rootRef.current?.contains(event.target)) {
-        setOpen(false)
-      }
-    }
-    document.addEventListener('pointerdown', closeOutside)
-    return () => { document.removeEventListener('pointerdown', closeOutside) }
-  }, [open])
+  useDismissOnOutsidePointer(rootRef, open, setOpen)
 
   // The clock only runs while an open list is showing something that moves.
   useEffect(() => {

+ 1 - 0
packages/client/ui-primitives/src/index.ts

@@ -13,6 +13,7 @@ export { Input } from './Input.tsx'
 export { Menu } from './Menu.tsx'
 export type { MenuEntry, MenuItem, MenuSeparator, MenuLabel } from './Menu.tsx'
 export { useAnchoredMaxHeight } from './useAnchoredMaxHeight.ts'
+export { useDismissOnOutsidePointer } from './useDismissOnOutsidePointer.ts'
 export { HoverCard } from './HoverCard.tsx'
 export { Modal } from './Modal.tsx'
 export { OnboardingSurface } from './OnboardingSurface.tsx'

+ 29 - 0
packages/client/ui-primitives/src/useDismissOnOutsidePointer.ts

@@ -0,0 +1,29 @@
+/**
+ * Outside-pointer dismissal for trigger-owned popovers (jobs list, Cordis
+ * panel): while the surface is open, a pointerdown outside the root closes it.
+ */
+import { useEffect } from 'react'
+import type { RefObject } from 'react'
+
+/**
+ * Close an open popover when a pointerdown lands outside its root element.
+ * @param root - element containing both the trigger and the open surface.
+ * @param open - whether the surface is showing; false detaches the listener.
+ * @param setOpen - state setter invoked with false on an outside pointerdown.
+ */
+export function useDismissOnOutsidePointer(
+  root: RefObject<HTMLElement | null>,
+  open: boolean,
+  setOpen: (open: boolean) => void,
+): void {
+  useEffect(() => {
+    if (!open) return
+    const closeOutside = (event: PointerEvent): void => {
+      if (event.target instanceof Node && !root.current?.contains(event.target)) {
+        setOpen(false)
+      }
+    }
+    document.addEventListener('pointerdown', closeOutside)
+    return () => { document.removeEventListener('pointerdown', closeOutside) }
+  }, [root, open, setOpen])
+}

+ 2 - 11
packages/extensions/ui-cordis/src/client/CordisPanel.tsx

@@ -4,7 +4,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react'
 import type { ButtonHTMLAttributes, ReactNode } from 'react'
 import {
   IconCheckOutline16, IconCloseOutline16, IconCordisPluginOutline14, IconPlayOutline16,
-  IconStopFill16, IconTrashOutline16, Tooltip,
+  IconStopFill16, IconTrashOutline16, Tooltip, useDismissOnOutsidePointer,
 } from '@deepseek-ai/dsh-client-ui-primitives'
 import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
 import type {} from '@deepseek-ai/dsh-client-ui-sidebar/client'
@@ -137,16 +137,7 @@ export function CordisPanel({
     return () => { window.removeEventListener('resize', place) }
   }, [open])
 
-  useEffect(() => {
-    if (!open) return
-    const closeOutside = (event: PointerEvent): void => {
-      if (event.target instanceof Node && !rootRef.current?.contains(event.target)) {
-        setOpen(false)
-      }
-    }
-    document.addEventListener('pointerdown', closeOutside)
-    return () => { document.removeEventListener('pointerdown', closeOutside) }
-  }, [open])
+  useDismissOnOutsidePointer(rootRef, open, setOpen)
 
   useEffect(() => {
     const now = new Set<ApprovalRequestId>()