Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 1 addition & 45 deletions packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,7 @@
*/
import type {RefObject} from 'react';
import {useState, useCallback, useEffect, useRef} from 'react';
import {useMutationObserver} from './useMutationObserver';

// Callback fires when the "hidden" attribute of a tabpanel changes
// See https://github.com/facebook/docusaurus/pull/7485
function useTabBecameVisibleCallback(
codeBlockRef: RefObject<HTMLPreElement | null>,
callback: () => void,
) {
const [hiddenTabElement, setHiddenTabElement] = useState<
Element | null | undefined
>();

const updateHiddenTabElement = useCallback(() => {
// No need to observe non-hidden tabs
// + we want to force a re-render when a tab becomes visible
setHiddenTabElement(
codeBlockRef.current?.closest('[role=tabpanel][hidden]'),
);
}, [codeBlockRef, setHiddenTabElement]);

useEffect(() => {
updateHiddenTabElement();
}, [updateHiddenTabElement]);

useMutationObserver(
hiddenTabElement,
(mutations: MutationRecord[]) => {
mutations.forEach((mutation) => {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'hidden'
) {
callback();
updateHiddenTabElement();
}
});
},
{
attributes: true,
characterData: false,
childList: false,
subtree: false,
},
);
}
import {useTabBecameVisibleCallback} from './useTabBecameVisibleCallback';

export type WordWrap = {
readonly codeBlockRef: RefObject<HTMLPreElement | null>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type {RefObject} from 'react';
import {useState, useCallback, useEffect} from 'react';
import {useMutationObserver} from './useMutationObserver';

// Callback fires when the "hidden" attribute of a tabpanel changes.
export function useTabBecameVisibleCallback(
elementRef: RefObject<Element | null>,
callback: () => void,
): void {
const [hiddenTabElement, setHiddenTabElement] = useState<
Element | null | undefined
>();

const updateHiddenTabElement = useCallback(() => {
// No need to observe non-hidden tabs.
// + we want to force a re-render when a tab becomes visible.
setHiddenTabElement(
elementRef.current?.closest('[role=tabpanel][hidden]'),
);
}, [elementRef, setHiddenTabElement]);

useEffect(() => {
updateHiddenTabElement();
}, [updateHiddenTabElement]);

useMutationObserver(
hiddenTabElement,
(mutations: MutationRecord[]) => {
mutations.forEach((mutation) => {
if (
mutation.type === 'attributes' &&
mutation.attributeName === 'hidden'
) {
callback();
updateHiddenTabElement();
}
});
},
{
attributes: true,
characterData: false,
childList: false,
subtree: false,
},
);
}
1 change: 1 addition & 0 deletions packages/docusaurus-theme-common/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export {useDateTimeFormat} from './utils/IntlUtils';
export {useHideableNavbar} from './hooks/useHideableNavbar';
export {useLockBodyScroll} from './hooks/useLockBodyScroll';
export {useCodeWordWrap} from './hooks/useCodeWordWrap';
export {useTabBecameVisibleCallback} from './hooks/useTabBecameVisibleCallback';
export {useBackToTopButton} from './hooks/useBackToTopButton';

export {useDocCardDescriptionCategoryItemsPlural} from './translations/docsTranslations';
Expand Down
4 changes: 3 additions & 1 deletion packages/docusaurus-theme-mermaid/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ async function renderMermaid({
export function useMermaidRenderResult({
text,
config: providedConfig,
renderCounter = 0,
}: {
text: string;
config?: MermaidConfig;
renderCounter?: number;
}): RenderResult | null {
const [result, setResult] = useState<RenderResult | null>(null);
const id = useMermaidId();
Expand All @@ -115,7 +117,7 @@ export function useMermaidRenderResult({
throw e;
});
});
}, [id, text, config]);
}, [id, text, config, renderCounter]);

return result;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @jest-environment jsdom
*/

// Jest doesn't allow pragma below other comments. https://github.com/facebook/jest/issues/12573
// eslint-disable-next-line header/header
import React from 'react';
import {act, render, waitFor} from '@testing-library/react';
import '@testing-library/jest-dom';
import Mermaid from '../index';

jest.mock(
'@docusaurus/ErrorBoundary',
() => {
function MockErrorBoundary({children}: {children: React.ReactNode}) {
return <>{children}</>;
}

return MockErrorBoundary;
},
{virtual: true},
);

jest.mock('@docusaurus/theme-common', () => ({
...jest.requireActual('@docusaurus/theme-common'),
useColorMode: () => ({colorMode: 'light'}),
useThemeConfig: () => ({
mermaid: {
theme: {
light: 'default',
dark: 'dark',
},
options: {},
},
}),
}));

jest.mock(
'@docusaurus/theme-common/internal',
() => ({
ErrorBoundaryErrorMessageFallback: () => null,
useTabBecameVisibleCallback:
require('../../../../../docusaurus-theme-common/src/hooks/useTabBecameVisibleCallback')
.useTabBecameVisibleCallback,
}),
{virtual: true},
);

jest.mock(
'@docusaurus/theme-mermaid/client',
() => ({
MermaidContainerClassName: 'mermaid',
useMermaidRenderResult: jest.fn(
({renderCounter}: {renderCounter: number}) => ({
svg: `<svg data-render-counter="${renderCounter}"></svg>`,
bindFunctions: undefined,
}),
),
}),
{virtual: true},
);

const {useMermaidRenderResult: mockUseMermaidRenderResult} = jest.requireMock(
'@docusaurus/theme-mermaid/client',
) as {
useMermaidRenderResult: jest.Mock;
};

describe('Mermaid', () => {
beforeEach(() => {
mockUseMermaidRenderResult.mockClear();
});

it('re-renders when a hidden tab becomes visible', async () => {
const tabPanel = document.createElement('div');
tabPanel.setAttribute('role', 'tabpanel');
tabPanel.setAttribute('hidden', '');
document.body.appendChild(tabPanel);

const {unmount} = render(<Mermaid value={'graph LR\n a --> b'} />, {
container: tabPanel,
});

expect(mockUseMermaidRenderResult).toHaveBeenCalledWith({
text: 'graph LR\n a --> b',
renderCounter: 0,
});
expect(tabPanel.querySelector('svg')).toHaveAttribute(
'data-render-counter',
'0',
);

await act(async () => {
tabPanel.removeAttribute('hidden');
});

await waitFor(() => {
expect(mockUseMermaidRenderResult).toHaveBeenCalledWith({
text: 'graph LR\n a --> b',
renderCounter: 1,
});
expect(tabPanel.querySelector('svg')).toHaveAttribute(
'data-render-counter',
'1',
);
});

unmount();
tabPanel.remove();
});
});
23 changes: 19 additions & 4 deletions packages/docusaurus-theme-mermaid/src/theme/Mermaid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/

import React, {useEffect, useRef, type ReactNode} from 'react';
import React, {useEffect, useRef, useState, type ReactNode} from 'react';
import ErrorBoundary from '@docusaurus/ErrorBoundary';
import {ErrorBoundaryErrorMessageFallback} from '@docusaurus/theme-common';
import {
ErrorBoundaryErrorMessageFallback,
useTabBecameVisibleCallback,
} from '@docusaurus/theme-common/internal';
import {
MermaidContainerClassName,
useMermaidRenderResult,
Expand All @@ -19,11 +22,15 @@ import styles from './styles.module.css';

function MermaidRenderResult({
renderResult,
onTabBecameVisible,
}: {
renderResult: RenderResult;
onTabBecameVisible: () => void;
}): ReactNode {
const ref = useRef<HTMLDivElement>(null);

useTabBecameVisibleCallback(ref, onTabBecameVisible);

useEffect(() => {
const div = ref.current!;
renderResult.bindFunctions?.(div);
Expand All @@ -40,11 +47,19 @@ function MermaidRenderResult({
}

function MermaidRenderer({value}: Props): ReactNode {
const renderResult = useMermaidRenderResult({text: value});
const [renderCounter, setRenderCounter] = useState(0);
const renderResult = useMermaidRenderResult({text: value, renderCounter});
if (renderResult === null) {
return null;
}
return <MermaidRenderResult renderResult={renderResult} />;
return (
<MermaidRenderResult
renderResult={renderResult}
onTabBecameVisible={() => {
setRenderCounter((value) => value + 1);
}}
/>
);
}

export default function Mermaid(props: Props): ReactNode {
Expand Down