diff --git a/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx b/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx index 1183362b2349..9c4b9f2ba5b9 100644 --- a/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx +++ b/packages/docusaurus-theme-common/src/contexts/navbarSecondaryMenu/content.tsx @@ -9,11 +9,11 @@ import React, { useState, useContext, useEffect, - useMemo, type ReactNode, type ComponentType, } from 'react'; import {ReactContextError} from '../../utils/reactUtils'; +import useShallowMemoObject from '../../hooks/useShallowMemoObject'; // This context represents a "global layout store". A component (usually a // layout component) can request filling this store through @@ -61,15 +61,6 @@ export function useNavbarSecondaryMenuContent(): Content { return value[0]; } -function useShallowMemoizedObject(obj: O) { - return useMemo( - () => obj, - // Is this safe? - // eslint-disable-next-line react-hooks/exhaustive-deps - [...Object.keys(obj), ...Object.values(obj)], - ); -} - /** * This component renders nothing by itself, but it fills the placeholder in the * generic secondary menu layout. This reduces coupling between the main layout @@ -94,7 +85,7 @@ export function NavbarSecondaryMenuFiller

({ const [, setContent] = context; // To avoid useless context re-renders, props are memoized shallowly - const memoizedProps = useShallowMemoizedObject(props); + const memoizedProps = useShallowMemoObject(props); useEffect(() => { // @ts-expect-error: this context is hard to type diff --git a/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts b/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts index 35443a1632b9..35d645322355 100644 --- a/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts +++ b/packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts @@ -6,14 +6,32 @@ */ import type {RefObject} from 'react'; import {useState, useCallback, useEffect, useRef} from 'react'; -import {useDynamicCallback} from '../utils/reactUtils'; import {useMutationObserver} from './useMutationObserver'; -function useHiddenAttributeMutationObserver( - target: Element | undefined | null, +// Callback fires when the "hidden" attribute of a tabpanel changes +// See https://github.com/facebook/docusaurus/pull/7485 +function useTabBecameVisibleCallback( + codeBlockRef: RefObject, callback: () => void, ) { - const hiddenAttributeCallback = useDynamicCallback( + 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 ( @@ -21,17 +39,17 @@ function useHiddenAttributeMutationObserver( mutation.attributeName === 'hidden' ) { callback(); + updateHiddenTabElement(); } }); }, + { + attributes: true, + characterData: false, + childList: false, + subtree: false, + }, ); - - useMutationObserver(target, hiddenAttributeCallback, { - attributes: true, - characterData: false, - childList: false, - subtree: false, - }); } export function useCodeWordWrap(): { @@ -42,7 +60,6 @@ export function useCodeWordWrap(): { } { const [isEnabled, setIsEnabled] = useState(false); const [isCodeScrollable, setIsCodeScrollable] = useState(false); - const [ancestor, setAncestor] = useState(); const codeBlockRef = useRef(null); const toggle = useCallback(() => { @@ -52,7 +69,6 @@ export function useCodeWordWrap(): { codeElement.removeAttribute('style'); } else { codeElement.style.whiteSpace = 'pre-wrap'; - codeElement.style.overflowWrap = 'anywhere'; } setIsEnabled((value) => !value); @@ -60,16 +76,13 @@ export function useCodeWordWrap(): { const updateCodeIsScrollable = useCallback(() => { const {scrollWidth, clientWidth} = codeBlockRef.current!; - // Allows code block to update scrollWidth and clientWidth after "hidden" - // attribute is removed - setAncestor(codeBlockRef.current?.closest('[hidden]')); const isScrollable = scrollWidth > clientWidth || codeBlockRef.current!.querySelector('code')!.hasAttribute('style'); setIsCodeScrollable(isScrollable); }, [codeBlockRef]); - useHiddenAttributeMutationObserver(ancestor, updateCodeIsScrollable); + useTabBecameVisibleCallback(codeBlockRef, updateCodeIsScrollable); useEffect(() => { updateCodeIsScrollable(); diff --git a/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts b/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts index 983234328443..8ce4e701dcfa 100644 --- a/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts +++ b/packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts @@ -4,35 +4,36 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ -import {useRef, useMemo, useEffect} from 'react'; -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import {useEffect} from 'react'; +import {useDynamicCallback} from '../utils/reactUtils'; +import useShallowMemoObject from './useShallowMemoObject'; + +type Options = MutationObserverInit; + +const DefaultOptions: Options = { + attributes: true, + characterData: true, + childList: true, + subtree: true, +}; export function useMutationObserver( target: Element | undefined | null, - callback: (mutations: MutationRecord[]) => void, - options = { - attributes: true, - characterData: true, - childList: true, - subtree: true, - }, + callback: MutationCallback, + options: Options = DefaultOptions, ): void { - const mutationObserver = useRef( - ExecutionEnvironment.canUseDOM ? new MutationObserver(callback) : undefined, - ); - const memoOptions = useMemo(() => options, [options]); + const stableCallback = useDynamicCallback(callback); - useEffect(() => { - const observer = mutationObserver.current; + // MutationObserver options are not nested much + // so this should be to memo options in 99% + // TODO handle options.attributeFilter array + const stableOptions: Options = useShallowMemoObject(options); - if (target && observer) { - observer.observe(target, memoOptions); + useEffect(() => { + const observer = new MutationObserver(stableCallback); + if (target) { + observer.observe(target, stableOptions); } - - return () => { - if (observer) { - observer.disconnect(); - } - }; - }, [target, memoOptions]); + return () => observer.disconnect(); + }, [target, stableCallback, stableOptions]); } diff --git a/packages/docusaurus-theme-common/src/hooks/useShallowMemoObject.ts b/packages/docusaurus-theme-common/src/hooks/useShallowMemoObject.ts new file mode 100644 index 000000000000..ca8da1f4a60b --- /dev/null +++ b/packages/docusaurus-theme-common/src/hooks/useShallowMemoObject.ts @@ -0,0 +1,17 @@ +/** + * 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 {useMemo} from 'react'; + +export default function useShallowMemoObject(obj: O): O { + return useMemo( + () => obj, + // Is this safe? + // eslint-disable-next-line react-hooks/exhaustive-deps + [...Object.keys(obj), ...Object.values(obj)], + ); +} diff --git a/website/_dogfooding/_pages tests/code-block-tests.mdx b/website/_dogfooding/_pages tests/code-block-tests.mdx index 95eb4e393923..44db91d49094 100644 --- a/website/_dogfooding/_pages tests/code-block-tests.mdx +++ b/website/_dogfooding/_pages tests/code-block-tests.mdx @@ -215,7 +215,7 @@ echo "hi" ```bash -mkdir this_will_test_whether_a_long_string_that_is_initially_hidden_will_have_the_option_to_wrap_when_made_visible +echo this will test whether a long string that is initially hidden will have the option to wrap when made visible ```