Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(theme-classic): inconsistent code block wrapping #7485

Merged
merged 6 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -61,15 +61,6 @@ export function useNavbarSecondaryMenuContent(): Content {
return value[0];
}

function useShallowMemoizedObject<O>(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
Expand All @@ -94,7 +85,7 @@ export function NavbarSecondaryMenuFiller<P extends object>({
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
Expand Down
47 changes: 30 additions & 17 deletions packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,50 @@
*/
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<HTMLPreElement>,
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 (
mutation.type === 'attributes' &&
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(): {
Expand All @@ -42,7 +60,6 @@ export function useCodeWordWrap(): {
} {
const [isEnabled, setIsEnabled] = useState(false);
const [isCodeScrollable, setIsCodeScrollable] = useState<boolean>(false);
const [ancestor, setAncestor] = useState<Element | null | undefined>();
const codeBlockRef = useRef<HTMLPreElement>(null);

const toggle = useCallback(() => {
Expand All @@ -52,24 +69,20 @@ export function useCodeWordWrap(): {
codeElement.removeAttribute('style');
} else {
codeElement.style.whiteSpace = 'pre-wrap';
codeElement.style.overflowWrap = 'anywhere';
}

setIsEnabled((value) => !value);
}, [codeBlockRef, isEnabled]);

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();
Expand Down
49 changes: 25 additions & 24 deletions packages/docusaurus-theme-common/src/hooks/useMutationObserver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<MutationObserver | undefined>(
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]);
}
17 changes: 17 additions & 0 deletions packages/docusaurus-theme-common/src/hooks/useShallowMemoObject.ts
Original file line number Diff line number Diff line change
@@ -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<O>(obj: O): O {
slorber marked this conversation as resolved.
Show resolved Hide resolved
return useMemo(
() => obj,
// Is this safe?
// eslint-disable-next-line react-hooks/exhaustive-deps
[...Object.keys(obj), ...Object.values(obj)],
);
}
2 changes: 1 addition & 1 deletion website/_dogfooding/_pages tests/code-block-tests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ echo "hi"
<TabItem value="long-tab" label="Long tab">

```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
```

</TabItem>
Expand Down