mirror of
https://github.com/facebook/docusaurus.git
synced 2025-06-20 11:37:52 +02:00
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:
parent
0a5354dc32
commit
948271a0ff
38 changed files with 555 additions and 317 deletions
|
@ -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);
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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},
|
||||
|
|
179
packages/docusaurus-theme-common/src/hooks/useTOCHighlight.ts
Normal file
179
packages/docusaurus-theme-common/src/hooks/useTOCHighlight.ts
Normal 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]);
|
||||
}
|
|
@ -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';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue