Skip to content

Commit

Permalink
refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
slorber committed Jun 2, 2022
1 parent 062348e commit 2eb5951
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 53 deletions.
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 {
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

0 comments on commit 2eb5951

Please sign in to comment.