From d87b82ef37b42adc1496c9b609b8fc8bcbb0c184 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Tue, 30 Jun 2026 15:48:25 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F(frontend)=20extract=20reusab?= =?UTF-8?q?le=20presenter=20slide=20content=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull the BlockNote rendering out of PresenterSlide into PresenterSlideContent so it can be reused by the title slide and the PDF export. Add a PresenterSlideData union (title | content) and a shared PresenterBlock type. --- .../components/PresenterSlide.tsx | 44 +-------- .../components/PresenterSlideContent.tsx | 95 +++++++++++++++++++ 2 files changed, 99 insertions(+), 40 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlideContent.tsx diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx index df550f4d2..9e12a755f 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlide.tsx @@ -1,11 +1,8 @@ -import { BlockNoteView } from '@blocknote/mantine'; -import { useCreateBlockNote } from '@blocknote/react'; -import { RefObject, useEffect, useRef } from 'react'; +import { RefObject, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { Box } from '@/components'; -import { blockNoteSchema } from '@/docs/doc-editor/components/BlockNoteEditor'; import { PRESENTER_FRAME_PADDING_Y, @@ -14,6 +11,8 @@ import { } from '../constants'; import { useFitScale } from '../hooks/useFitScale'; +import { PresenterSlideContent } from './PresenterSlideContent'; + interface PresenterSlideProps { blocks: unknown[]; frameRef: RefObject; @@ -47,12 +46,6 @@ const outerCss = css` overflow-x: hidden; background: white; transition: opacity ${PRESENTER_SLIDE_FADE_MS}ms ease; - /* Hide editor chrome that may leak through despite editable={false} */ - .bn-side-menu, - .bn-formatting-toolbar, - .bn-slash-menu { - display: none !important; - } `; // The stage absorbs the un-scaled inner's layout box. Its explicit height @@ -85,28 +78,6 @@ export const PresenterSlide = ({ }: PresenterSlideProps) => { const { t } = useTranslation(); const innerRef = useRef(null); - const editor = useCreateBlockNote({ - initialContent: - // BlockNote rejects an empty initialContent array — fall back to one empty paragraph. - blocks.length > 0 - ? (blocks as NonNullable< - Parameters[0] - >['initialContent']) - : undefined, - schema: blockNoteSchema, - }); - - // ProseMirror adds role="textbox" and contenteditable on its root even - // when editable=false, making the SR announce "editing, autocomplete" on - // focus. Strip those attributes so the slide reads as plain content. - useEffect(() => { - const pm = innerRef.current?.querySelector('.ProseMirror'); - if (pm) { - pm.removeAttribute('role'); - pm.removeAttribute('contenteditable'); - pm.setAttribute('tabindex', '-1'); - } - }, []); const fit = useFitScale(innerRef, frameRef); @@ -136,14 +107,7 @@ export const PresenterSlide = ({ > - + diff --git a/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlideContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlideContent.tsx new file mode 100644 index 000000000..27190e6f5 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-presenter/components/PresenterSlideContent.tsx @@ -0,0 +1,95 @@ +import { BlockNoteView } from '@blocknote/mantine'; +import { useCreateBlockNote } from '@blocknote/react'; +import { CSSProperties, Ref, useEffect, useRef } from 'react'; +import { css } from 'styled-components'; + +import { Box } from '@/components'; +import { blockNoteSchema } from '@/docs/doc-editor/components/BlockNoteEditor'; + +import type { PresenterBlock } from '../types'; + +interface PresenterSlideContentProps { + blocks: PresenterBlock[]; + className?: string; + innerRef?: Ref; + style?: CSSProperties; +} + +const slideContentCss = css` + .bn-side-menu, + .bn-formatting-toolbar, + .bn-slash-menu { + display: none !important; + } +`; + +const setRefValue = ( + ref: Ref | undefined, + node: HTMLDivElement | null, +) => { + if (!ref) { + return; + } + + if (typeof ref === 'function') { + ref(node); + return; + } + + (ref as { current: HTMLDivElement | null }).current = node; +}; + +export const PresenterSlideContent = ({ + blocks, + className, + innerRef, + style, +}: PresenterSlideContentProps) => { + const contentRef = useRef(null); + const editor = useCreateBlockNote({ + initialContent: + // BlockNote rejects an empty initialContent array — fall back to one empty paragraph. + blocks.length > 0 + ? (blocks as NonNullable< + Parameters[0] + >['initialContent']) + : undefined, + schema: blockNoteSchema, + }); + + // Even with `editable={false}`, BlockNote/ProseMirror currently still renders + // the editor node with `role="textbox"` and `contenteditable`. For presenter + // slides that is wrong - the content is presentation, not an editable field - + // and it pollutes the accessibility tree. Strip those, but keep the content + // tabbable so keyboard users can reach and scroll long slides. (Observed + // BlockNote behaviour, not a guarantee; revisit on upgrades.) + useEffect(() => { + const pm = contentRef.current?.querySelector('.ProseMirror'); + if (pm) { + pm.removeAttribute('role'); + pm.removeAttribute('contenteditable'); + pm.setAttribute('tabindex', '0'); + } + }, []); + + return ( + { + contentRef.current = node; + setRefValue(innerRef, node); + }} + className={className} + $css={slideContentCss} + style={style} + > + + + ); +};