mirror of
https://github.com/facebook/docusaurus.git
synced 2025-07-31 07:18:59 +02:00
fix(theme-classic): inconsistent code block wrapping (#7485)
Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
parent
7dd822becb
commit
b215ad0e1b
6 changed files with 202 additions and 15 deletions
|
@ -9,11 +9,10 @@ import React, {
|
||||||
useState,
|
useState,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
type ComponentType,
|
type ComponentType,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import {ReactContextError} from '../../utils/reactUtils';
|
import {ReactContextError, useShallowMemoObject} from '../../utils/reactUtils';
|
||||||
|
|
||||||
// This context represents a "global layout store". A component (usually a
|
// This context represents a "global layout store". A component (usually a
|
||||||
// layout component) can request filling this store through
|
// layout component) can request filling this store through
|
||||||
|
@ -61,15 +60,6 @@ export function useNavbarSecondaryMenuContent(): Content {
|
||||||
return value[0];
|
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
|
* This component renders nothing by itself, but it fills the placeholder in the
|
||||||
* generic secondary menu layout. This reduces coupling between the main layout
|
* generic secondary menu layout. This reduces coupling between the main layout
|
||||||
|
@ -94,7 +84,7 @@ export function NavbarSecondaryMenuFiller<P extends object>({
|
||||||
const [, setContent] = context;
|
const [, setContent] = context;
|
||||||
|
|
||||||
// To avoid useless context re-renders, props are memoized shallowly
|
// To avoid useless context re-renders, props are memoized shallowly
|
||||||
const memoizedProps = useShallowMemoizedObject(props);
|
const memoizedProps = useShallowMemoObject(props);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// @ts-expect-error: this context is hard to type
|
// @ts-expect-error: this context is hard to type
|
||||||
|
|
|
@ -4,9 +4,53 @@
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {RefObject} from 'react';
|
import type {RefObject} from 'react';
|
||||||
import {useState, useCallback, useEffect, useRef} 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(): {
|
export function useCodeWordWrap(): {
|
||||||
readonly codeBlockRef: RefObject<HTMLPreElement>;
|
readonly codeBlockRef: RefObject<HTMLPreElement>;
|
||||||
|
@ -38,6 +82,8 @@ export function useCodeWordWrap(): {
|
||||||
setIsCodeScrollable(isScrollable);
|
setIsCodeScrollable(isScrollable);
|
||||||
}, [codeBlockRef]);
|
}, [codeBlockRef]);
|
||||||
|
|
||||||
|
useTabBecameVisibleCallback(codeBlockRef, updateCodeIsScrollable);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateCodeIsScrollable();
|
updateCodeIsScrollable();
|
||||||
}, [isEnabled, updateCodeIsScrollable]);
|
}, [isEnabled, updateCodeIsScrollable]);
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/**
|
||||||
|
* 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, useShallowMemoObject} from '../utils/reactUtils';
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {renderHook} from '@testing-library/react-hooks';
|
import {renderHook} from '@testing-library/react-hooks';
|
||||||
import {usePrevious} from '../reactUtils';
|
import {usePrevious, useShallowMemoObject} from '../reactUtils';
|
||||||
|
|
||||||
describe('usePrevious', () => {
|
describe('usePrevious', () => {
|
||||||
it('returns the previous value of a variable', () => {
|
it('returns the previous value of a variable', () => {
|
||||||
|
@ -20,3 +20,37 @@ describe('usePrevious', () => {
|
||||||
expect(result.current).toBe(2);
|
expect(result.current).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useShallowMemoObject', () => {
|
||||||
|
it('can memoize object', () => {
|
||||||
|
const someObj = {hello: 'world'};
|
||||||
|
const someArray = ['hello', 'world'];
|
||||||
|
|
||||||
|
const obj1 = {a: 1, b: '2', someObj, someArray};
|
||||||
|
const {result, rerender} = renderHook((val) => useShallowMemoObject(val), {
|
||||||
|
initialProps: obj1,
|
||||||
|
});
|
||||||
|
expect(result.current).toBe(obj1);
|
||||||
|
|
||||||
|
const obj2 = {a: 1, b: '2', someObj, someArray};
|
||||||
|
rerender(obj2);
|
||||||
|
expect(result.current).toBe(obj1);
|
||||||
|
|
||||||
|
const obj3 = {a: 1, b: '2', someObj, someArray};
|
||||||
|
rerender(obj3);
|
||||||
|
expect(result.current).toBe(obj1);
|
||||||
|
|
||||||
|
// Current implementation is basic and sensitive to order
|
||||||
|
const obj4 = {b: '2', a: 1, someObj, someArray};
|
||||||
|
rerender(obj4);
|
||||||
|
expect(result.current).toBe(obj4);
|
||||||
|
|
||||||
|
const obj5 = {b: '2', a: 1, someObj, someArray};
|
||||||
|
rerender(obj5);
|
||||||
|
expect(result.current).toBe(obj4);
|
||||||
|
|
||||||
|
const obj6 = {b: '2', a: 1, someObj: {...someObj}, someArray};
|
||||||
|
rerender(obj6);
|
||||||
|
expect(result.current).toBe(obj6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {useCallback, useEffect, useLayoutEffect, useRef} from 'react';
|
import {useCallback, useEffect, useLayoutEffect, useMemo, useRef} from 'react';
|
||||||
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -74,3 +74,23 @@ export class ReactContextError extends Error {
|
||||||
} is called outside the <${providerName}>. ${additionalInfo ?? ''}`;
|
} is called outside the <${providerName}>. ${additionalInfo ?? ''}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shallow-memoize an object
|
||||||
|
*
|
||||||
|
* This means the returned object will be the same as the previous render
|
||||||
|
* if the attribute names and identities did not change.
|
||||||
|
*
|
||||||
|
* This works for simple cases: when attributes are primitives or stable objects
|
||||||
|
*
|
||||||
|
* @param obj
|
||||||
|
*/
|
||||||
|
export function useShallowMemoObject<O>(obj: O): O {
|
||||||
|
return useMemo(
|
||||||
|
() => obj,
|
||||||
|
// Is this safe?
|
||||||
|
// TODO make this implementation not order-dependent?
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[...Object.keys(obj), ...Object.values(obj)],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import CodeBlock from '@theme/CodeBlock';
|
import CodeBlock from '@theme/CodeBlock';
|
||||||
import BrowserWindow from '@site/src/components/BrowserWindow';
|
import BrowserWindow from '@site/src/components/BrowserWindow';
|
||||||
|
import Tabs from '@theme/Tabs';
|
||||||
|
import TabItem from '@theme/TabItem';
|
||||||
|
|
||||||
# Code block tests
|
# Code block tests
|
||||||
|
|
||||||
|
@ -190,3 +192,60 @@ function PageLayout(props) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Code block wrapping tests
|
||||||
|
|
||||||
|
[// spell-checker:disable]: #
|
||||||
|
|
||||||
|
```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]: #
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue