Merge branch 'master' into fix/agent-preset-ui-flow
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
{
|
||||
"name": "@deepseek-ai/dsh-client-ui-primitives",
|
||||
"description": "Pure React atoms for the dsh web UI: controls, icons, markdown, and JSON inspectors (zero cordis)",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"version": "0.0.1-rc.1",
|
||||
"publishConfig": {
|
||||
"access": "restricted"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
||||
"directory": "packages/client/ui-primitives"
|
||||
},
|
||||
"type": "module",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/types/index.d.ts",
|
||||
@@ -45,7 +52,7 @@
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@types/react": "~18.3.1",
|
||||
"@types/react-dom": "~18.3.0",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
},
|
||||
"files": [
|
||||
"lib/index.js",
|
||||
@@ -53,7 +60,7 @@
|
||||
"lib/types/**/*.d.ts"
|
||||
],
|
||||
"peerDependencies": {
|
||||
"@deepseek-ai/dsh-invariants": "^0.0.1",
|
||||
"cordis": "^4.0.0-rc.7"
|
||||
"@deepseek-ai/dsh-invariants": "workspace:^",
|
||||
"@deepseek-ai/cordis": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@ interface AnchorProps {
|
||||
onBlur?: FocusEventHandler | undefined
|
||||
}
|
||||
|
||||
type TooltipLabel = string | (() => string)
|
||||
|
||||
/**
|
||||
* Attach a hover/focus tooltip to an anchor element.
|
||||
* @param props.label - bubble text.
|
||||
* @param props.label - bubble text, or a resolver evaluated only while the bubble is visible.
|
||||
* @param props.side - placement relative to the anchor (default 'right').
|
||||
* @param props.delayMs - hover delay in milliseconds; keyboard focus remains immediate.
|
||||
* @param props.disabled - suppress the bubble while true; the anchor renders identically so
|
||||
@@ -34,7 +36,7 @@ interface AnchorProps {
|
||||
* @param props.children - a single anchor element; its own ref (callback or object) is forwarded alongside the tooltip's.
|
||||
* @returns the cloned anchor plus a fixed-position bubble while hovered/focused.
|
||||
*/
|
||||
export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false, children }: { label: string; side?: TooltipSide; delayMs?: number; disabled?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false, children }: { label: TooltipLabel; side?: TooltipSide; delayMs?: number; disabled?: boolean; children: ReactElement<AnchorProps> }) {
|
||||
const anchor = useRef<HTMLElement | null>(null)
|
||||
// React 18 keeps the element's ref outside props; forward it so wrapping an
|
||||
// anchor in Tooltip never silently severs the owner's ref.
|
||||
@@ -46,6 +48,9 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
}, [childRef])
|
||||
const [pos, setPos] = useState<{ x: number; y: number } | null>(null)
|
||||
const bubble = useRef<HTMLSpanElement | null>(null)
|
||||
const resolvedLabel = pos === null
|
||||
? null
|
||||
: typeof label === 'function' ? label() : label
|
||||
// Horizontal viewport clamp: fixed positioning knows nothing about edges, so
|
||||
// a centered bubble near the right edge would clip. Each measurement resets
|
||||
// the base position before applying a direct style offset, allowing a shorter
|
||||
@@ -67,7 +72,7 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
clamp()
|
||||
window.addEventListener('resize', clamp)
|
||||
return () => { window.removeEventListener('resize', clamp) }
|
||||
}, [label, pos])
|
||||
}, [pos, resolvedLabel])
|
||||
const showTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
// Hover and focus are independent triggers: the bubble hides only after
|
||||
// BOTH clear (hovering away from a focused anchor must not drop it).
|
||||
@@ -128,7 +133,7 @@ export function Tooltip({ label, side = 'right', delayMs = 0, disabled = false,
|
||||
})}
|
||||
{pos !== null && (
|
||||
<span ref={bubble} className={css.bubble} data-side={side} style={{ left: pos.x, top: pos.y }} role="tooltip">
|
||||
{label}
|
||||
{resolvedLabel}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
/* jscpd:ignore-start */
|
||||
import type { Context } from 'cordis'
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
const PACKAGE_NAME = '@deepseek-ai/dsh-client-ui-primitives'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Context } from 'cordis'
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import * as PrimitivesInvariant from '@deepseek-ai/dsh-client-ui-primitives/invariant'
|
||||
import InvariantService from '@deepseek-ai/dsh-invariants'
|
||||
|
||||
|
||||
@@ -6,6 +6,27 @@ import { Tooltip } from '@deepseek-ai/dsh-client-ui-primitives'
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('Tooltip', () => {
|
||||
it('resolves lazy labels only after the bubble becomes visible', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const label = vi.fn(() => 'Timing details')
|
||||
render(
|
||||
<Tooltip label={label} delayMs={500}>
|
||||
<button type="button">anchor</button>
|
||||
</Tooltip>,
|
||||
)
|
||||
expect(label).not.toHaveBeenCalled()
|
||||
fireEvent.mouseEnter(screen.getByText('anchor'))
|
||||
act(() => { vi.advanceTimersByTime(499) })
|
||||
expect(label).not.toHaveBeenCalled()
|
||||
act(() => { vi.advanceTimersByTime(1) })
|
||||
expect(screen.getByRole('tooltip').textContent).toBe('Timing details')
|
||||
expect(label).toHaveBeenCalledOnce()
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('can delay pointer hover without delaying keyboard focus', () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user