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

Co-authored-by: sebastienlorber <lorber.sebastien@gmail.com>
This commit is contained in:
dpang314 2022-06-02 09:56:54 -04:00 committed by GitHub
parent 7dd822becb
commit b215ad0e1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 202 additions and 15 deletions

View file

@ -9,11 +9,10 @@ import React, {
useState,
useContext,
useEffect,
useMemo,
type ReactNode,
type ComponentType,
} from 'react';
import {ReactContextError} from '../../utils/reactUtils';
import {ReactContextError, useShallowMemoObject} from '../../utils/reactUtils';
// This context represents a "global layout store". A component (usually a
// layout component) can request filling this store through
@ -61,15 +60,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
@ -94,7 +84,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

View file

@ -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>;
@ -38,6 +82,8 @@ export function useCodeWordWrap(): {
setIsCodeScrollable(isScrollable);
}, [codeBlockRef]);
useTabBecameVisibleCallback(codeBlockRef, updateCodeIsScrollable);
useEffect(() => {
updateCodeIsScrollable();
}, [isEnabled, updateCodeIsScrollable]);

View file

@ -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]);
}

View file

@ -6,7 +6,7 @@
*/
import {renderHook} from '@testing-library/react-hooks';
import {usePrevious} from '../reactUtils';
import {usePrevious, useShallowMemoObject} from '../reactUtils';
describe('usePrevious', () => {
it('returns the previous value of a variable', () => {
@ -20,3 +20,37 @@ describe('usePrevious', () => {
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);
});
});

View file

@ -5,7 +5,7 @@
* 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';
/**
@ -74,3 +74,23 @@ export class ReactContextError extends Error {
} 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)],
);
}