From b6825bc703f1814a0e49f5d5abf8e38f55c1547f Mon Sep 17 00:00:00 2001 From: Erik Michelson Date: Tue, 4 Aug 2026 14:49:07 +0200 Subject: [PATCH] fix(renderer): printing properly from iframe and host React hooks may not be called inside other react hooks due to the "rule of hooks". Furthermore, with the recent iframe-communicator changes, the hook for printing from outside the iframe failed - no matter whether on normal documents or slides. This change therefore uses two distinct hooks that represent the event listeners for each side, instead of a shared one. Signed-off-by: Erik Michelson --- .../editor-page/editor-page-content.tsx | 4 +- .../hooks/use-print-keyboard-shortcut.spec.ts | 40 +++++++++++++++++++ .../hooks/use-print-keyboard-shortcut.ts | 25 +++++++----- .../render-page/render-page-content.tsx | 4 +- frontend/src/redux/print-mode/slice.ts | 4 +- 5 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.spec.ts diff --git a/frontend/src/components/editor-page/editor-page-content.tsx b/frontend/src/components/editor-page/editor-page-content.tsx index 363fe5d5d..f29fd7287 100644 --- a/frontend/src/components/editor-page/editor-page-content.tsx +++ b/frontend/src/components/editor-page/editor-page-content.tsx @@ -18,7 +18,7 @@ import { PrintWarning } from './print-warning/print-warning' import React, { useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import './print.scss' -import { usePrintKeyboardShortcut } from './hooks/use-print-keyboard-shortcut' +import { usePrintIframeKeyboardShortcut } from './hooks/use-print-keyboard-shortcut' import { NoteType } from '@hedgedoc/commons' import { useApplicationState } from '../../hooks/common/use-application-state' import { buildCursorLineScrollState } from './synced-scroll/cursor-line-scroll-state' @@ -33,7 +33,7 @@ export enum ScrollSource { */ export const EditorPageContent: React.FC = () => { useTranslation() - usePrintKeyboardShortcut() + usePrintIframeKeyboardShortcut() const scrollSource = useRef(ScrollSource.EDITOR) const [editorScrollState, onMarkdownRendererScroll] = useScrollState(scrollSource, ScrollSource.EDITOR) diff --git a/frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.spec.ts b/frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.spec.ts new file mode 100644 index 000000000..d58480350 --- /dev/null +++ b/frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.spec.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { fireEvent, renderHook } from '@testing-library/react' +import { usePrintIframeKeyboardShortcut, usePrintSelfKeyboardShortcut } from './use-print-keyboard-shortcut' + +const printIframe = jest.fn() +const printSelf = jest.fn() + +jest.mock('../utils/print-iframe', () => ({ + usePrintIframe: () => printIframe, + usePrintSelf: () => printSelf +})) + +describe('usePrintKeyboardShortcut', () => { + beforeEach(() => { + printIframe.mockClear() + printSelf.mockClear() + }) + + it('prints the renderer iframe when the print shortcut is pressed', () => { + renderHook(() => usePrintIframeKeyboardShortcut()) + + fireEvent.keyDown(window, { ctrlKey: true, key: 'p' }) + + expect(printIframe).toHaveBeenCalledTimes(1) + expect(printSelf).not.toHaveBeenCalled() + }) + + it('prints the current window when the print shortcut is pressed in the renderer iframe', () => { + renderHook(() => usePrintSelfKeyboardShortcut()) + + fireEvent.keyDown(window, { ctrlKey: true, key: 'p' }) + + expect(printSelf).toHaveBeenCalledTimes(1) + expect(printIframe).not.toHaveBeenCalled() + }) +}) diff --git a/frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.ts b/frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.ts index 8628e2548..5fa530905 100644 --- a/frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.ts +++ b/frontend/src/components/editor-page/hooks/use-print-keyboard-shortcut.ts @@ -4,26 +4,33 @@ * SPDX-License-Identifier: AGPL-3.0-only */ import { usePrintIframe, usePrintSelf } from '../utils/print-iframe' -import { useCallback, useEffect, useMemo } from 'react' +import { useCallback, useEffect } from 'react' /** * Hook to listen for the print keyboard shortcut and print the content of the renderer iframe. */ -export const usePrintKeyboardShortcut = (): void => { - const isIframe = useMemo(() => window.top !== window.self, []) +export const usePrintIframeKeyboardShortcut = (): void => { + const printIframe = usePrintIframe() + usePrintShortcut(printIframe) +} +/** + * Hook to listen for the print keyboard shortcut and print the renderer content from within the iframe. + */ +export const usePrintSelfKeyboardShortcut = (): void => { + const printSelf = usePrintSelf() + usePrintShortcut(printSelf) +} + +const usePrintShortcut = (print: () => void): void => { const handlePrint = useCallback( (event: KeyboardEvent): void => { if (event.key === 'p' && (event.ctrlKey || event.metaKey)) { event.preventDefault() - if (isIframe) { - usePrintSelf()() - } else { - usePrintIframe()() - } + print() } }, - [isIframe] + [print] ) useEffect(() => { diff --git a/frontend/src/components/render-page/render-page-content.tsx b/frontend/src/components/render-page/render-page-content.tsx index 263dd12e0..e287e42fa 100644 --- a/frontend/src/components/render-page/render-page-content.tsx +++ b/frontend/src/components/render-page/render-page-content.tsx @@ -18,7 +18,7 @@ import type { RevealOptions } from 'reveal.js' import { EventEmitter2 } from 'eventemitter2' import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react' import { setPrintMode } from '../../redux/print-mode/methods' -import { usePrintKeyboardShortcut } from '../editor-page/hooks/use-print-keyboard-shortcut' +import { usePrintSelfKeyboardShortcut } from '../editor-page/hooks/use-print-keyboard-shortcut' /** * Wraps the markdown rendering in an iframe. @@ -127,7 +127,7 @@ export const RenderPageContent: React.FC = () => { ) ) - usePrintKeyboardShortcut() + usePrintSelfKeyboardShortcut() const onMakeScrollSource = useCallback(() => { sendScrolling.current = true diff --git a/frontend/src/redux/print-mode/slice.ts b/frontend/src/redux/print-mode/slice.ts index b288febcc..a4a5feeb0 100644 --- a/frontend/src/redux/print-mode/slice.ts +++ b/frontend/src/redux/print-mode/slice.ts @@ -11,9 +11,7 @@ const printModeSlice = createSlice({ name: 'printMode', initialState, reducers: { - setPrintMode: (state, action: PayloadAction) => { - state = action.payload - } + setPrintMode: (_state, action: PayloadAction) => action.payload } })