mirror of
https://github.com/suitenumerique/docs.git
synced 2026-08-17 21:25:43 +02:00
♻️(frontend) extract reusable presenter slide content component
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.
This commit is contained in:
+4
-40
@@ -1,11 +1,8 @@
|
|||||||
import { BlockNoteView } from '@blocknote/mantine';
|
import { RefObject, useRef } from 'react';
|
||||||
import { useCreateBlockNote } from '@blocknote/react';
|
|
||||||
import { RefObject, useEffect, useRef } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
import { blockNoteSchema } from '@/docs/doc-editor/components/BlockNoteEditor';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PRESENTER_FRAME_PADDING_Y,
|
PRESENTER_FRAME_PADDING_Y,
|
||||||
@@ -14,6 +11,8 @@ import {
|
|||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { useFitScale } from '../hooks/useFitScale';
|
import { useFitScale } from '../hooks/useFitScale';
|
||||||
|
|
||||||
|
import { PresenterSlideContent } from './PresenterSlideContent';
|
||||||
|
|
||||||
interface PresenterSlideProps {
|
interface PresenterSlideProps {
|
||||||
blocks: unknown[];
|
blocks: unknown[];
|
||||||
frameRef: RefObject<HTMLDivElement | null>;
|
frameRef: RefObject<HTMLDivElement | null>;
|
||||||
@@ -47,12 +46,6 @@ const outerCss = css`
|
|||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
background: white;
|
background: white;
|
||||||
transition: opacity ${PRESENTER_SLIDE_FADE_MS}ms ease;
|
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
|
// The stage absorbs the un-scaled inner's layout box. Its explicit height
|
||||||
@@ -85,28 +78,6 @@ export const PresenterSlide = ({
|
|||||||
}: PresenterSlideProps) => {
|
}: PresenterSlideProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const innerRef = useRef<HTMLDivElement>(null);
|
const innerRef = useRef<HTMLDivElement>(null);
|
||||||
const editor = useCreateBlockNote({
|
|
||||||
initialContent:
|
|
||||||
// BlockNote rejects an empty initialContent array — fall back to one empty paragraph.
|
|
||||||
blocks.length > 0
|
|
||||||
? (blocks as NonNullable<
|
|
||||||
Parameters<typeof useCreateBlockNote>[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);
|
const fit = useFitScale(innerRef, frameRef);
|
||||||
|
|
||||||
@@ -136,14 +107,7 @@ export const PresenterSlide = ({
|
|||||||
>
|
>
|
||||||
<Box $css={stageCss} style={stageStyle}>
|
<Box $css={stageCss} style={stageStyle}>
|
||||||
<Box ref={innerRef} $css={innerCss} style={innerStyle}>
|
<Box ref={innerRef} $css={innerCss} style={innerStyle}>
|
||||||
<BlockNoteView
|
<PresenterSlideContent blocks={blocks} />
|
||||||
editor={editor}
|
|
||||||
editable={false}
|
|
||||||
theme="light"
|
|
||||||
formattingToolbar={false}
|
|
||||||
slashMenu={false}
|
|
||||||
comments={false}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
+95
@@ -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<HTMLDivElement>;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slideContentCss = css`
|
||||||
|
.bn-side-menu,
|
||||||
|
.bn-formatting-toolbar,
|
||||||
|
.bn-slash-menu {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const setRefValue = (
|
||||||
|
ref: Ref<HTMLDivElement> | 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<HTMLDivElement | null>(null);
|
||||||
|
const editor = useCreateBlockNote({
|
||||||
|
initialContent:
|
||||||
|
// BlockNote rejects an empty initialContent array — fall back to one empty paragraph.
|
||||||
|
blocks.length > 0
|
||||||
|
? (blocks as NonNullable<
|
||||||
|
Parameters<typeof useCreateBlockNote>[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 (
|
||||||
|
<Box
|
||||||
|
ref={(node: HTMLDivElement | null) => {
|
||||||
|
contentRef.current = node;
|
||||||
|
setRefValue(innerRef, node);
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
$css={slideContentCss}
|
||||||
|
style={style}
|
||||||
|
>
|
||||||
|
<BlockNoteView
|
||||||
|
editor={editor}
|
||||||
|
editable={false}
|
||||||
|
theme="light"
|
||||||
|
formattingToolbar={false}
|
||||||
|
slashMenu={false}
|
||||||
|
comments={false}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user