test: improve test coverage; reorder theme-common files (#6956)

* test: improve test coverage; reorder theme-common files

* no need for this
This commit is contained in:
Joshua Chen 2022-03-22 15:33:55 +08:00 committed by GitHub
parent 0a5354dc32
commit 948271a0ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 555 additions and 317 deletions

View file

@ -9,14 +9,10 @@ import {useState, useCallback, useRef} from 'react';
import {useLocationChange} from '../utils/useLocationChange';
import {useScrollPosition} from '../utils/scrollUtils';
type UseHideableNavbarReturns = {
export function useHideableNavbar(hideOnScroll: boolean): {
readonly navbarRef: (node: HTMLElement | null) => void;
readonly isNavbarVisible: boolean;
};
export default function useHideableNavbar(
hideOnScroll: boolean,
): UseHideableNavbarReturns {
} {
const [isNavbarVisible, setIsNavbarVisible] = useState(hideOnScroll);
const isFocusedAnchor = useRef(false);
const navbarHeight = useRef(0);

View file

@ -11,9 +11,11 @@ import './styles.css';
export const keyboardFocusedClassName = 'navigation-with-keyboard';
// This hook detect keyboard focus indicator to not show outline for mouse users
// Inspired by https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2
export default function useKeyboardNavigation(): void {
/**
* Detect keyboard focus indicator to not show outline for mouse users
* Inspired by https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2
*/
export function useKeyboardNavigation(): void {
useEffect(() => {
function handleOutlineStyles(e: MouseEvent | KeyboardEvent) {
if (e.type === 'keydown' && (e as KeyboardEvent).key === 'Tab') {

View file

@ -7,7 +7,7 @@
import {useEffect} from 'react';
export default function useLockBodyScroll(lock: boolean = true): void {
export function useLockBodyScroll(lock: boolean = true): void {
useEffect(() => {
document.body.style.overflow = lock ? 'hidden' : 'visible';

View file

@ -6,10 +6,10 @@
*/
import defaultTheme from 'prism-react-renderer/themes/palenight';
import {useColorMode} from '../utils/colorModeUtils';
import {useColorMode} from '../contexts/colorMode';
import {useThemeConfig} from '../utils/useThemeConfig';
export default function usePrismTheme(): typeof defaultTheme {
export function usePrismTheme(): typeof defaultTheme {
const {prism} = useThemeConfig();
const {colorMode} = useColorMode();
const lightModeTheme = prism.theme || defaultTheme;

View file

@ -11,13 +11,11 @@ import {useCallback, useEffect, useState} from 'react';
const SEARCH_PARAM_QUERY = 'q';
interface UseSearchPageReturn {
export function useSearchPage(): {
searchQuery: string;
setSearchQuery: (newSearchQuery: string) => void;
generateSearchPageLink: (targetSearchQuery: string) => string;
}
export default function useSearchPage(): UseSearchPageReturn {
} {
const history = useHistory();
const {
siteConfig: {baseUrl},

View file

@ -0,0 +1,179 @@
/**
* 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, useRef} from 'react';
import {useThemeConfig} from '../utils/useThemeConfig';
// TODO make the hardcoded theme-classic classnames configurable (or add them
// to ThemeClassNames?)
// If the anchor has no height and is just a "marker" in the dom; we'll use the
// parent (normally the link text) rect boundaries instead
function getVisibleBoundingClientRect(element: HTMLElement): DOMRect {
const rect = element.getBoundingClientRect();
const hasNoHeight = rect.top === rect.bottom;
if (hasNoHeight) {
return getVisibleBoundingClientRect(element.parentNode as HTMLElement);
}
return rect;
}
/**
* Considering we divide viewport into 2 zones of each 50vh, this returns true
* if an element is in the first zone (ie, appear in viewport, near the top)
*/
function isInViewportTopHalf(boundingRect: DOMRect) {
return boundingRect.top > 0 && boundingRect.bottom < window.innerHeight / 2;
}
function getAnchors({
minHeadingLevel,
maxHeadingLevel,
}: {
minHeadingLevel: number;
maxHeadingLevel: number;
}) {
const selectors = [];
for (let i = minHeadingLevel; i <= maxHeadingLevel; i += 1) {
selectors.push(`h${i}.anchor`);
}
return Array.from(
document.querySelectorAll(selectors.join()),
) as HTMLElement[];
}
function getActiveAnchor(
anchors: HTMLElement[],
{
anchorTopOffset,
}: {
anchorTopOffset: number;
},
): Element | null {
// Naming is hard: The "nextVisibleAnchor" is the first anchor that appear
// under the viewport top boundary. It does not mean this anchor is visible
// yet, but if user continues scrolling down, it will be the first to become
// visible
const nextVisibleAnchor = anchors.find((anchor) => {
const boundingRect = getVisibleBoundingClientRect(anchor);
return boundingRect.top >= anchorTopOffset;
});
if (nextVisibleAnchor) {
const boundingRect = getVisibleBoundingClientRect(nextVisibleAnchor);
// If anchor is in the top half of the viewport: it is the one we consider
// "active" (unless it's too close to the top and and soon to be scrolled
// outside viewport)
if (isInViewportTopHalf(boundingRect)) {
return nextVisibleAnchor;
}
// If anchor is in the bottom half of the viewport, or under the viewport,
// we consider the active anchor is the previous one. This is because the
// main text appearing in the user screen mostly belong to the previous
// anchor. Returns null for the first anchor, see
// https://github.com/facebook/docusaurus/issues/5318
return anchors[anchors.indexOf(nextVisibleAnchor) - 1] ?? null;
}
// no anchor under viewport top? (ie we are at the bottom of the page)
// => highlight the last anchor found
return anchors[anchors.length - 1] ?? null;
}
function getLinkAnchorValue(link: HTMLAnchorElement): string {
return decodeURIComponent(link.href.substring(link.href.indexOf('#') + 1));
}
function getLinks(linkClassName: string) {
return Array.from(
document.getElementsByClassName(linkClassName),
) as HTMLAnchorElement[];
}
function getNavbarHeight(): number {
// Not ideal to obtain actual height this way
// Using TS ! (not ?) because otherwise a bad selector would be un-noticed
return document.querySelector('.navbar')!.clientHeight;
}
function useAnchorTopOffsetRef() {
const anchorTopOffsetRef = useRef<number>(0);
const {
navbar: {hideOnScroll},
} = useThemeConfig();
useEffect(() => {
anchorTopOffsetRef.current = hideOnScroll ? 0 : getNavbarHeight();
}, [hideOnScroll]);
return anchorTopOffsetRef;
}
export type TOCHighlightConfig = {
linkClassName: string;
linkActiveClassName: string;
minHeadingLevel: number;
maxHeadingLevel: number;
};
export function useTOCHighlight(config: TOCHighlightConfig | undefined): void {
const lastActiveLinkRef = useRef<HTMLAnchorElement | undefined>(undefined);
const anchorTopOffsetRef = useAnchorTopOffsetRef();
useEffect(() => {
if (!config) {
// no-op, highlighting is disabled
return () => {};
}
const {
linkClassName,
linkActiveClassName,
minHeadingLevel,
maxHeadingLevel,
} = config;
function updateLinkActiveClass(link: HTMLAnchorElement, active: boolean) {
if (active) {
if (lastActiveLinkRef.current && lastActiveLinkRef.current !== link) {
lastActiveLinkRef.current?.classList.remove(linkActiveClassName);
}
link.classList.add(linkActiveClassName);
lastActiveLinkRef.current = link;
// link.scrollIntoView({block: 'nearest'});
} else {
link.classList.remove(linkActiveClassName);
}
}
function updateActiveLink() {
const links = getLinks(linkClassName);
const anchors = getAnchors({minHeadingLevel, maxHeadingLevel});
const activeAnchor = getActiveAnchor(anchors, {
anchorTopOffset: anchorTopOffsetRef.current,
});
const activeLink = links.find(
(link) => activeAnchor && activeAnchor.id === getLinkAnchorValue(link),
);
links.forEach((link) => {
updateLinkActiveClass(link, link === activeLink);
});
}
document.addEventListener('scroll', updateActiveLink);
document.addEventListener('resize', updateActiveLink);
updateActiveLink();
return () => {
document.removeEventListener('scroll', updateActiveLink);
document.removeEventListener('resize', updateActiveLink);
};
}, [config, anchorTopOffsetRef]);
}

View file

@ -41,7 +41,7 @@ const DevSimulateSSR = process.env.NODE_ENV === 'development' && true;
// This hook returns an enum value on purpose!
// We don't want it to return the actual width value, for resize perf reasons
// We only want to re-render once a breakpoint is crossed
export default function useWindowSize(): WindowSize {
export function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>(() => {
if (DevSimulateSSR) {
return 'ssr';