Skip to content

Commit

Permalink
feat(theme-common): JSDoc for all APIs (#6974)
Browse files Browse the repository at this point in the history
* feat(theme-common): JSDoc for all APIs

* fix tests
  • Loading branch information
Josh-Cena committed Mar 23, 2022
1 parent 4103fef commit b456a64
Show file tree
Hide file tree
Showing 48 changed files with 871 additions and 679 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,19 @@ import React, {

const DefaultAnimationEasing = 'ease-in-out';

export type UseCollapsibleConfig = {
/**
* This hook is a very thin wrapper around a `useState`.
*/
export function useCollapsible({
initialState,
}: {
/** The initial state. Will be non-collapsed by default. */
initialState: boolean | (() => boolean);
};

export type UseCollapsibleReturns = {
}): {
collapsed: boolean;
setCollapsed: Dispatch<SetStateAction<boolean>>;
toggleCollapsed: () => void;
};

// This hook just define the state
export function useCollapsible({
initialState,
}: UseCollapsibleConfig): UseCollapsibleReturns {
} {
const [collapsed, setCollapsed] = useState(initialState ?? false);

const toggleCollapsed = useCallback(() => {
Expand Down Expand Up @@ -152,8 +151,10 @@ type CollapsibleElementType = React.ElementType<
Pick<React.HTMLAttributes<unknown>, 'className' | 'onTransitionEnd' | 'style'>
>;

// Prevent hydration layout shift before animations are handled imperatively
// with JS
/**
* Prevent hydration layout shift before animations are handled imperatively
* with JS
*/
function getSSRStyle(collapsed: boolean) {
if (ExecutionEnvironment.canUseDOM) {
return undefined;
Expand All @@ -162,16 +163,27 @@ function getSSRStyle(collapsed: boolean) {
}

type CollapsibleBaseProps = {
/** The actual DOM element to be used in the markup. */
as?: CollapsibleElementType;
/** Initial collapsed state. */
collapsed: boolean;
children: ReactNode;
/** Configuration of animation, like `duration` and `easing` */
animation?: CollapsibleAnimationConfig;
/**
* A callback fired when the collapse transition animation ends. Receives
* the **new** collapsed state: e.g. when
* expanding, `collapsed` will be `false`. You can use this for some "cleanup"
* like applying new styles when the container is fully expanded.
*/
onCollapseTransitionEnd?: (collapsed: boolean) => void;
/** Class name for the underlying DOM element. */
className?: string;

// This is mostly useful for details/summary component where ssrStyle is not
// needed (as details are hidden natively) and can mess up with the default
// native behavior of the browser when JS fails to load or is disabled
/**
* This is mostly useful for details/summary component where ssrStyle is not
* needed (as details are hidden natively) and can mess up with the browser's
* native behavior when JS fails to load or is disabled
*/
disableSSRStyle?: boolean;
};

Expand Down Expand Up @@ -233,14 +245,20 @@ function CollapsibleLazy({collapsed, ...props}: CollapsibleBaseProps) {
}

type CollapsibleProps = CollapsibleBaseProps & {
// Lazy allows to delay the rendering when collapsed => it will render
// children only after hydration, on first expansion
// Required prop: it forces to think if content should be server-rendered
// or not! This has perf impact on the SSR output and html file sizes
// See https://github.com/facebook/docusaurus/issues/4753
/**
* Delay rendering of the content till first expansion. Marked as required to
* force us to think if content should be server-rendered or not. This has
* perf impact since it reduces html file sizes, but could undermine SEO.
* @see https://github.com/facebook/docusaurus/issues/4753
*/
lazy: boolean;
};

/**
* A headless component providing smooth and uniform collapsing behavior. The
* component will be invisible (zero height) when collapsed. Doesn't provide
* interactivity by itself: collapse state is toggled through props.
*/
export function Collapsible({lazy, ...props}: CollapsibleProps): JSX.Element {
const Comp = lazy ? CollapsibleLazy : CollapsibleBase;
return <Comp {...props} />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,14 @@ function hasParent(node: HTMLElement | null, parent: HTMLElement): boolean {
}

export type DetailsProps = {
/** Summary is provided as props, including the wrapping `<summary>` tag */
summary?: ReactElement;
} & ComponentProps<'details'>;

/**
* A mostly un-styled `<details>` element with smooth collapsing. Provides some
* very lightweight styles, but you should bring your UI.
*/
export function Details({
summary,
children,
Expand All @@ -45,8 +50,8 @@ export function Details({
const {collapsed, setCollapsed} = useCollapsible({
initialState: !props.open,
});
// Use a separate prop because it must be set only after animation completes
// Otherwise close anim won't work
// Use a separate state for the actual details prop, because it must be set
// only after animation completes, otherwise close animations won't work
const [open, setOpen] = useState(props.open);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* 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 React from 'react';
import {renderHook} from '@testing-library/react-hooks';
import {useDocsSidebar, DocsSidebarProvider} from '../docsSidebar';
import type {PropSidebar} from '@docusaurus/plugin-content-docs';

describe('useDocsSidebar', () => {
it('throws if context provider is missing', () => {
expect(
() => renderHook(() => useDocsSidebar()).result.current,
).toThrowErrorMatchingInlineSnapshot(
`"Hook useDocsSidebar is called outside the <DocsSidebarProvider>. "`,
);
});

it('reads value from context provider', () => {
const sidebar: PropSidebar = [];
const {result} = renderHook(() => useDocsSidebar(), {
wrapper: ({children}) => (
<DocsSidebarProvider sidebar={sidebar}>{children}</DocsSidebarProvider>
),
});
expect(result.current).toBe(sidebar);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/**
* 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 React from 'react';
import {renderHook} from '@testing-library/react-hooks';
import {useDocsVersion, DocsVersionProvider} from '../docsVersion';
import type {PropVersionMetadata} from '@docusaurus/plugin-content-docs';

function testVersion(data?: Partial<PropVersionMetadata>): PropVersionMetadata {
return {
version: 'versionName',
label: 'Version Label',
className: 'version className',
badge: true,
banner: 'unreleased',
docs: {},
docsSidebars: {},
isLast: false,
pluginId: 'default',
...data,
};
}

describe('useDocsVersion', () => {
it('throws if context provider is missing', () => {
expect(
() => renderHook(() => useDocsVersion()).result.current,
).toThrowErrorMatchingInlineSnapshot(
`"Hook useDocsVersion is called outside the <DocsVersionProvider>. "`,
);
});

it('reads value from context provider', () => {
const version = testVersion();
const {result} = renderHook(() => useDocsVersion(), {
wrapper: ({children}) => (
<DocsVersionProvider version={version}>{children}</DocsVersionProvider>
),
});
expect(result.current).toBe(version);
});
});
28 changes: 13 additions & 15 deletions packages/docusaurus-theme-common/src/contexts/announcementBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,18 @@ const isDismissedInStorage = () =>
const setDismissedInStorage = (bool: boolean) =>
AnnouncementBarDismissStorage.set(String(bool));

type AnnouncementBarAPI = {
type ContextValue = {
/** Whether the announcement bar should be displayed. */
readonly isActive: boolean;
/**
* Callback fired when the user closes the announcement. Will be saved.
*/
readonly close: () => void;
};

const useAnnouncementBarContextValue = (): AnnouncementBarAPI => {
const Context = React.createContext<ContextValue | null>(null);

function useContextValue(): ContextValue {
const {announcementBar} = useThemeConfig();
const isBrowser = useIsBrowser();

Expand Down Expand Up @@ -93,27 +99,19 @@ const useAnnouncementBarContextValue = (): AnnouncementBarAPI => {
}),
[announcementBar, isClosed, handleClose],
);
};

const AnnouncementBarContext = React.createContext<AnnouncementBarAPI | null>(
null,
);
}

export function AnnouncementBarProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const value = useAnnouncementBarContextValue();
return (
<AnnouncementBarContext.Provider value={value}>
{children}
</AnnouncementBarContext.Provider>
);
const value = useContextValue();
return <Context.Provider value={value}>{children}</Context.Provider>;
}

export function useAnnouncementBar(): AnnouncementBarAPI {
const api = useContext(AnnouncementBarContext);
export function useAnnouncementBar(): ContextValue {
const api = useContext(Context);
if (!api) {
throw new ReactContextError('AnnouncementBarProvider');
}
Expand Down
34 changes: 14 additions & 20 deletions packages/docusaurus-theme-common/src/contexts/colorMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {createStorageSlot} from '../utils/storageUtils';
import {useThemeConfig} from '../utils/useThemeConfig';

type ColorModeContextValue = {
type ContextValue = {
/** Current color mode. */
readonly colorMode: ColorMode;
/** Set new color mode. */
readonly setColorMode: (colorMode: ColorMode) => void;

// TODO legacy APIs kept for retro-compatibility: deprecate them
Expand All @@ -30,6 +32,8 @@ type ColorModeContextValue = {
readonly setDarkTheme: () => void;
};

const Context = React.createContext<ContextValue | undefined>(undefined);

const ColorModeStorageKey = 'theme';
const ColorModeStorage = createStorageSlot(ColorModeStorageKey);

Expand All @@ -44,18 +48,16 @@ export type ColorMode = typeof ColorModes[keyof typeof ColorModes];
const coerceToColorMode = (colorMode?: string | null): ColorMode =>
colorMode === ColorModes.dark ? ColorModes.dark : ColorModes.light;

const getInitialColorMode = (defaultMode: ColorMode | undefined): ColorMode => {
if (!ExecutionEnvironment.canUseDOM) {
return coerceToColorMode(defaultMode);
}
return coerceToColorMode(document.documentElement.getAttribute('data-theme'));
};
const getInitialColorMode = (defaultMode: ColorMode | undefined): ColorMode =>
ExecutionEnvironment.canUseDOM
? coerceToColorMode(document.documentElement.getAttribute('data-theme'))
: coerceToColorMode(defaultMode);

const storeColorMode = (newColorMode: ColorMode) => {
ColorModeStorage.set(coerceToColorMode(newColorMode));
};

function useColorModeContextValue(): ColorModeContextValue {
function useContextValue(): ContextValue {
const {
colorMode: {defaultMode, disableSwitch, respectPrefersColorScheme},
} = useThemeConfig();
Expand Down Expand Up @@ -153,25 +155,17 @@ function useColorModeContextValue(): ColorModeContextValue {
);
}

const ColorModeContext = React.createContext<ColorModeContextValue | undefined>(
undefined,
);

export function ColorModeProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const contextValue = useColorModeContextValue();
return (
<ColorModeContext.Provider value={contextValue}>
{children}
</ColorModeContext.Provider>
);
const value = useContextValue();
return <Context.Provider value={value}>{children}</Context.Provider>;
}

export function useColorMode(): ColorModeContextValue {
const context = useContext(ColorModeContext);
export function useColorMode(): ContextValue {
const context = useContext(Context);
if (context == null) {
throw new ReactContextError(
'ColorModeProvider',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,30 @@
import React, {type ReactNode, useMemo, useState, useContext} from 'react';
import {ReactContextError} from '../utils/reactUtils';

const EmptyContext: unique symbol = Symbol('EmptyContext');
const Context = React.createContext<
DocSidebarItemsExpandedState | typeof EmptyContext
>(EmptyContext);
type DocSidebarItemsExpandedState = {
type ContextValue = {
/**
* The item that the user last opened, `null` when there's none open. On
* initial render, it will always be `null`, which doesn't necessarily mean
* there's no category open (can have 0, 1, or many being initially open).
*/
expandedItem: number | null;
/**
* Set the currently expanded item, when the user opens one. Set the value to
* `null` when the user closes an open category.
*/
setExpandedItem: (a: number | null) => void;
};

const EmptyContext: unique symbol = Symbol('EmptyContext');
const Context = React.createContext<ContextValue | typeof EmptyContext>(
EmptyContext,
);

/**
* Should be used to wrap one sidebar category level. This provider syncs the
* expanded states of all sibling categories, and categories can choose to
* collapse itself if another one is expanded.
*/
export function DocSidebarItemsExpandedStateProvider({
children,
}: {
Expand All @@ -31,10 +46,10 @@ export function DocSidebarItemsExpandedStateProvider({
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
}

export function useDocSidebarItemsExpandedState(): DocSidebarItemsExpandedState {
const contextValue = useContext(Context);
if (contextValue === EmptyContext) {
export function useDocSidebarItemsExpandedState(): ContextValue {
const value = useContext(Context);
if (value === EmptyContext) {
throw new ReactContextError('DocSidebarItemsExpandedStateProvider');
}
return contextValue;
return value;
}

0 comments on commit b456a64

Please sign in to comment.