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 5 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
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
48 changes: 47 additions & 1 deletion packages/docusaurus-theme-common/src/hooks/useCodeWordWrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,53 @@
* 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, 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>,
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,
},
);
}

export function useCodeWordWrap(): {
readonly codeBlockRef: RefObject<HTMLPreElement>;
Expand Down Expand Up @@ -38,6 +82,8 @@ export function useCodeWordWrap(): {
setIsCodeScrollable(isScrollable);
}, [codeBlockRef]);

useTabBecameVisibleCallback(codeBlockRef, updateCodeIsScrollable);

useEffect(() => {
updateCodeIsScrollable();
}, [isEnabled, updateCodeIsScrollable]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* 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 {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: MutationCallback,
options: Options = DefaultOptions,
): void {
const stableCallback = useDynamicCallback(callback);

// 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);

useEffect(() => {
const observer = new MutationObserver(stableCallback);
if (target) {
observer.observe(target, stableOptions);
}
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)],
);
}
59 changes: 59 additions & 0 deletions website/_dogfooding/_pages tests/code-block-tests.mdx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import CodeBlock from '@theme/CodeBlock';
import BrowserWindow from '@site/src/components/BrowserWindow';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';

# Code block tests

Expand Down Expand Up @@ -190,3 +192,60 @@ function PageLayout(props) {
);
}
```

## Code block wrapping tests

[// spell-checker:disable]: #
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, I didn't know this is a thing 🤔


```bash
mkdir this_is_a_loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong_string_to_test_code_block_wrapping
echo "this is a long string made up of many separate words that should be broken between words when possible"
curl https://docusaurus.io/tests/pages/code-block-tests
```

<Tabs>

<TabItem value="short-tab-1" label="Short tab">

```bash
echo "hi"
```

</TabItem>
<TabItem value="long-tab" label="Long tab">

```bash
echo this will test whether a long string that is initially hidden will have the option to wrap when made visible
```

</TabItem>

<TabItem value="short-tab-2" label="Short tab">

```bash
rm short_initially_hidden_string
```

</TabItem>
</Tabs>

<Tabs>

<TabItem value="long-tab" label="Long tab">

```bash
echo medium_length_string_will_have_the_option_to_wrap_after_window_resized_while_it_is_hidden
```

</TabItem>

<TabItem value="short-tab" label="Short tab">

```bash
echo "short_initially_hidden_string"
```

</TabItem>
</Tabs>

[// spell-checker:enable]: #