feat: US-005 - Hover-to-highlight interaction on desktop
This commit is contained in:
+1
-1
@@ -96,7 +96,7 @@
|
|||||||
"Verify in browser: hover on/off roles cycles highlight cleanly with no stuck states"
|
"Verify in browser: hover on/off roles cycles highlight cleanly with no stuck states"
|
||||||
],
|
],
|
||||||
"priority": 5,
|
"priority": 5,
|
||||||
"passes": false,
|
"passes": true,
|
||||||
"notes": "The interaction handlers are in the D3 useEffect where mouseenter/mouseleave/click are attached to node groups. supportsCoarsePointer is a module-level window.matchMedia('(pointer: coarse)').matches check. For fine pointer (desktop): mouseenter calls applyGraphHighlight(nodeId) + fires onNodeHover(nodeId), mouseleave calls applyGraphHighlight(null) + fires onNodeHover(null). Remove the click handler's pin/unpin toggle for fine pointer. For coarse pointer (touch): keep existing tap-to-pin unchanged. The pinnedNodeId useState remains but only gets set on coarse pointer or keyboard interactions. The callbacksRef pattern prevents stale closures — use it for onNodeHover. The onNodeHover callback propagates to DashboardLayout for bidirectional highlighting (graph→timeline). Use the d3-viz skill."
|
"notes": "The interaction handlers are in the D3 useEffect where mouseenter/mouseleave/click are attached to node groups. supportsCoarsePointer is a module-level window.matchMedia('(pointer: coarse)').matches check. For fine pointer (desktop): mouseenter calls applyGraphHighlight(nodeId) + fires onNodeHover(nodeId), mouseleave calls applyGraphHighlight(null) + fires onNodeHover(null). Remove the click handler's pin/unpin toggle for fine pointer. For coarse pointer (touch): keep existing tap-to-pin unchanged. The pinnedNodeId useState remains but only gets set on coarse pointer or keyboard interactions. The callbacksRef pattern prevents stale closures — use it for onNodeHover. The onNodeHover callback propagates to DashboardLayout for bidirectional highlighting (graph→timeline). Use the d3-viz skill."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -95,3 +95,18 @@
|
|||||||
- Math.round() wraps all scaled values to avoid sub-pixel rendering artifacts
|
- Math.round() wraps all scaled values to avoid sub-pixel rendering artifacts
|
||||||
- Accessibility overlay buttons in the React JSX also need scaling — they use base constants directly, not the D3-scoped variables
|
- Accessibility overlay buttons in the React JSX also need scaling — they use base constants directly, not the D3-scoped variables
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 2026-02-16 - US-005
|
||||||
|
- Changed mouseenter handler: on desktop (supportsCoarsePointer === false), calls applyGraphHighlight(d.id) + onNodeHover(d.id) for hover-to-highlight
|
||||||
|
- Changed mouseleave handler: resets to highlightedNodeId ?? null (external timeline state or resting), NOT pinnedNodeId
|
||||||
|
- Changed click handler: desktop clicks only fire detail callbacks (onRoleClick/onSkillClick), no pin toggle
|
||||||
|
- Touch (coarse pointer) retains tap-to-pin toggle unchanged inside click handler
|
||||||
|
- pinnedNodeId state only set/cleared for touch interactions
|
||||||
|
- Files changed: src/components/CareerConstellation.tsx
|
||||||
|
- Browser verified: hover on "Interim Head" → 12 connected skills at fill-opacity 0.9, 9 dimmed at opacity 0.15; hover off → all reset to resting (fill-opacity 0.35, label opacity 0.5); desktop click → no pin state
|
||||||
|
- **Learnings for future iterations:**
|
||||||
|
- D3 mouseenter/mouseleave events require dispatchEvent() in Playwright headless — native page.hover() on SVG <g> elements doesn't reliably trigger D3 handlers
|
||||||
|
- Role rect fill-opacity 0.12 IS the resting state (initialized at line 384), not a dimmed state — don't confuse with skill resting at 0.35
|
||||||
|
- mouseleave should reset to highlightedNodeId (external prop) not pinnedNodeId — on desktop there is no pin, so fallback is null (resting)
|
||||||
|
- The supportsCoarsePointer guard at top of each handler cleanly separates desktop/touch paths without duplicating the handler
|
||||||
|
---
|
||||||
|
|||||||
@@ -561,24 +561,25 @@ const CareerConstellation: React.FC<CareerConstellationProps> = ({
|
|||||||
|
|
||||||
nodeSelection.on('mouseleave', function() {
|
nodeSelection.on('mouseleave', function() {
|
||||||
if (supportsCoarsePointer) return
|
if (supportsCoarsePointer) return
|
||||||
applyGraphHighlight(highlightedNodeId ?? pinnedNodeId)
|
applyGraphHighlight(highlightedNodeId ?? null)
|
||||||
callbacksRef.current.onNodeHover?.(pinnedNodeId)
|
callbacksRef.current.onNodeHover?.(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
nodeSelection.on('click', function(_event, d) {
|
nodeSelection.on('click', function(_event, d) {
|
||||||
if (supportsCoarsePointer && pinnedNodeId !== d.id) {
|
if (supportsCoarsePointer) {
|
||||||
setPinnedNodeId(d.id)
|
// Touch: tap-to-pin toggle
|
||||||
applyGraphHighlight(d.id)
|
if (pinnedNodeId === d.id) {
|
||||||
if (d.type === 'role') {
|
setPinnedNodeId(null)
|
||||||
callbacksRef.current.onNodeHover?.(d.id)
|
applyGraphHighlight(null)
|
||||||
|
callbacksRef.current.onNodeHover?.(null)
|
||||||
|
} else {
|
||||||
|
setPinnedNodeId(d.id)
|
||||||
|
applyGraphHighlight(d.id)
|
||||||
|
callbacksRef.current.onNodeHover?.(d.type === 'role' ? d.id : null)
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPinned = pinnedNodeId === d.id ? null : d.id
|
// Fire detail callbacks for both desktop and touch
|
||||||
setPinnedNodeId(newPinned)
|
|
||||||
callbacksRef.current.onNodeHover?.(d.type === 'role' ? newPinned : null)
|
|
||||||
|
|
||||||
if (d.type === 'role') {
|
if (d.type === 'role') {
|
||||||
callbacksRef.current.onRoleClick(d.id)
|
callbacksRef.current.onRoleClick(d.id)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user