feat(memory-graph): add onNodeSelect callback for selection changes

Selection lived entirely inside MemoryGraph, so a host embedding the graph had
no way to observe which node is selected outside of slideshow mode
(onSlideshowNodeChange) or the open-document action (onOpenDocument). That makes
common integrations awkward: driving a side panel, syncing selection to the URL,
or firing analytics on select.

Add an optional onNodeSelect(nodeId) prop that fires whenever the selection
changes, regardless of source (click, keyboard navigation, slideshow, or
clearing), with null when nothing is selected. It is emitted from an effect
keyed on the selection state and reads the callback through a ref, so a changing
callback identity does not re-fire it and the selection stays uncontrolled. The
initial mount is skipped so a host does not receive a spurious null before any
interaction. Purely additive and backward compatible.
This commit is contained in:
abhay-codes07 2026-07-24 11:42:46 +05:30
parent 3487666481
commit b569fe2075
No known key found for this signature in database
3 changed files with 24 additions and 0 deletions

View file

@ -66,6 +66,7 @@ function App() {
| `error` | `Error \| null` | Error to display |
| `loadMoreDocuments` | `() => Promise<void>` | Function to load more data |
| `highlightDocumentIds` | `string[]` | IDs of documents to highlight |
| `onNodeSelect` | `(nodeId: string \| null) => void` | Called when the selected node changes (click, keyboard, slideshow, or cleared) |
## Documentation

View file

@ -39,6 +39,7 @@ export function MemoryGraph({
colors: colorOverrides,
totalCount,
onOpenDocument,
onNodeSelect,
labels: labelOverrides,
layering,
}: MemoryGraphProps) {
@ -73,6 +74,21 @@ export function MemoryGraph({
// Used as a dependency proxy to recalculate popover positions
const [viewportVersion, setViewportVersion] = useState(0)
// Surface selection changes to the host without making selection controlled.
// Keyed on selectedNode only; the callback is read through a ref so a changing
// callback identity does not re-fire it. The initial mount is skipped so a
// host does not receive a spurious null before any interaction.
const onNodeSelectRef = useRef(onNodeSelect)
onNodeSelectRef.current = onNodeSelect
const hasEmittedSelectionRef = useRef(false)
useEffect(() => {
if (!hasEmittedSelectionRef.current) {
hasEmittedSelectionRef.current = true
return
}
onNodeSelectRef.current?.(selectedNode)
}, [selectedNode])
// Limit documents so total node count (documents + their memories) stays under maxNodes
const limitedDocuments = useMemo(() => {
if (!maxNodes || documents.length === 0) return documents

View file

@ -216,6 +216,13 @@ export interface MemoryGraphProps {
totalCount?: number
/** Callback when user wants to view full document content */
onOpenDocument?: (documentId: string) => void
/**
* Called whenever the selected node changes, by click, keyboard navigation,
* slideshow, or clearing the selection (nodeId is null when nothing is
* selected). Lets a host observe selection without owning the state, e.g. to
* drive a side panel or sync the URL.
*/
onNodeSelect?: (nodeId: string | null) => void
/** Custom user-facing labels (partial) - merged with defaults */
labels?: MemoryGraphLabels
/** Overlay layering controls */