diff --git a/packages/react/src/SuperDocEditor.test.tsx b/packages/react/src/SuperDocEditor.test.tsx index c93b3af722..b944661d85 100644 --- a/packages/react/src/SuperDocEditor.test.tsx +++ b/packages/react/src/SuperDocEditor.test.tsx @@ -117,6 +117,46 @@ describe('SuperDocEditor', () => { { timeout: 5000 }, ); }); + + it('should route onTransaction through the latest callback after rerender', async () => { + const ref = createRef(); + const onReady = vi.fn(); + const firstOnTransaction = vi.fn(); + const secondOnTransaction = vi.fn(); + + const { rerender } = render(); + + await waitFor(() => expect(onReady).toHaveBeenCalled(), { timeout: 5000 }); + + const instance = ref.current?.getInstance(); + expect(instance).toBeTruthy(); + + const transactionEvent = { + editor: {}, + sourceEditor: {}, + transaction: { docChanged: true }, + surface: 'body', + }; + + const firstCallCountBeforeManualDispatch = firstOnTransaction.mock.calls.length; + (instance as any).config.onTransaction(transactionEvent); + + expect(firstOnTransaction).toHaveBeenLastCalledWith(transactionEvent); + expect(firstOnTransaction).toHaveBeenCalledTimes(firstCallCountBeforeManualDispatch + 1); + expect(secondOnTransaction).not.toHaveBeenCalled(); + + rerender(); + + expect(ref.current?.getInstance()).toBe(instance); + + const firstCallCountBeforeRerenderDispatch = firstOnTransaction.mock.calls.length; + const secondCallCountBeforeManualDispatch = secondOnTransaction.mock.calls.length; + (instance as any).config.onTransaction(transactionEvent); + + expect(firstOnTransaction).toHaveBeenCalledTimes(firstCallCountBeforeRerenderDispatch); + expect(secondOnTransaction).toHaveBeenLastCalledWith(transactionEvent); + expect(secondOnTransaction).toHaveBeenCalledTimes(secondCallCountBeforeManualDispatch + 1); + }); }); describe('onEditorDestroy', () => { diff --git a/packages/react/src/SuperDocEditor.tsx b/packages/react/src/SuperDocEditor.tsx index b215bdacc0..b022552456 100644 --- a/packages/react/src/SuperDocEditor.tsx +++ b/packages/react/src/SuperDocEditor.tsx @@ -17,6 +17,7 @@ import type { SuperDocReadyEvent, SuperDocEditorCreateEvent, SuperDocEditorUpdateEvent, + SuperDocTransactionEvent, SuperDocContentErrorEvent, SuperDocExceptionEvent, } from './types'; @@ -46,6 +47,7 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef(null); @@ -192,6 +196,11 @@ function SuperDocEditorInner(props: SuperDocEditorProps, ref: ForwardedRef { + if (!destroyed) { + callbacksRef.current.onTransaction?.(event); + } + }, onContentError: (event: SuperDocContentErrorEvent) => { if (!destroyed) { callbacksRef.current.onContentError?.(event); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index dafad66ad4..eba3573a70 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -24,6 +24,7 @@ export type { SuperDocReadyEvent, SuperDocEditorCreateEvent, SuperDocEditorUpdateEvent, + SuperDocTransactionEvent, SuperDocContentErrorEvent, SuperDocExceptionEvent, } from './types'; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index dfb96a27ce..709a83f6da 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -67,6 +67,24 @@ export interface SuperDocEditorUpdateEvent { sectionType?: string | null; } +/** Event passed to onTransaction callback. Mirrors superdoc's EditorTransactionEvent. */ +export interface SuperDocTransactionEvent { + /** The primary editor associated with the transaction. For header/footer edits, this is the main body editor. */ + editor: Editor; + /** The editor instance that emitted the transaction. For body edits, this matches `editor`. */ + sourceEditor: Editor; + /** The ProseMirror transaction or transaction-like payload emitted by the source editor. */ + transaction: any; + /** Time spent applying the transaction, in milliseconds. */ + duration?: number; + /** The surface where the transaction originated. */ + surface: EditorSurface; + /** Relationship ID for header/footer edits. */ + headerId?: string | null; + /** Header/footer variant (`default`, `first`, `even`, `odd`) when available. */ + sectionType?: string | null; +} + /** Event passed to onContentError callback */ export interface SuperDocContentErrorEvent { error: Error; @@ -107,6 +125,7 @@ type ExplicitCallbackProps = | 'onEditorCreate' | 'onEditorDestroy' | 'onEditorUpdate' + | 'onTransaction' | 'onContentError' | 'onException'; @@ -127,6 +146,9 @@ export interface CallbackProps { /** Callback when document content is updated */ onEditorUpdate?: (event: SuperDocEditorUpdateEvent) => void; + /** Callback when a transaction is emitted */ + onTransaction?: (event: SuperDocTransactionEvent) => void; + /** Callback when there is a content parsing error */ onContentError?: (event: SuperDocContentErrorEvent) => void;