/**
* 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, {
useState,
useEffect,
useCallback,
useMemo,
useContext,
type ReactNode,
} from 'react';
import useIsBrowser from '@docusaurus/useIsBrowser';
import {createStorageSlot} from '../utils/storageUtils';
import {ReactContextError} from '../utils/reactUtils';
import {useThemeConfig} from '../utils/useThemeConfig';
export const AnnouncementBarDismissStorageKey =
'docusaurus.announcement.dismiss';
const AnnouncementBarIdStorageKey = 'docusaurus.announcement.id';
const AnnouncementBarDismissStorage = createStorageSlot(
AnnouncementBarDismissStorageKey,
);
const IdStorage = createStorageSlot(AnnouncementBarIdStorageKey);
const isDismissedInStorage = () =>
AnnouncementBarDismissStorage.get() === 'true';
const setDismissedInStorage = (bool: boolean) =>
AnnouncementBarDismissStorage.set(String(bool));
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 Context = React.createContext<ContextValue | null>(null);
function useContextValue(): ContextValue {
const {announcementBar} = useThemeConfig();
const isBrowser = useIsBrowser();
const [isClosed, setClosed] = useState(() =>
isBrowser
? // On client navigation: init with local storage value
isDismissedInStorage()
: // On server/hydration: always visible to prevent layout shifts (will be hidden with css if needed)
false,
);
// Update state after hydration
useEffect(() => {
setClosed(isDismissedInStorage());
}, []);
const handleClose = useCallback(() => {
setDismissedInStorage(true);
setClosed(true);
}, []);
useEffect(() => {
if (!announcementBar) {
return;
}
const {id} = announcementBar;
let viewedId = IdStorage.get();
// Retrocompatibility due to spelling mistake of default id
// see https://github.com/facebook/docusaurus/issues/3338
// cSpell:ignore annoucement
if (viewedId === 'annoucement-bar') {
viewedId = 'announcement-bar';
}
const isNewAnnouncement = id !== viewedId;
IdStorage.set(id);
if (isNewAnnouncement) {
setDismissedInStorage(false);
}
if (isNewAnnouncement || !isDismissedInStorage()) {
setClosed(false);
}
}, [announcementBar]);
return useMemo(
() => ({
isActive: !!announcementBar && !isClosed,
close: handleClose,
}),
[announcementBar, isClosed, handleClose],
);
}
export function AnnouncementBarProvider({
children,
}: {
children: ReactNode;
}): JSX.Element {
const value = useContextValue();
return <Context.Provider value={value}>{children}</Context.Provider>;
}
export function useAnnouncementBar(): ContextValue {
const api = useContext(Context);
if (!api) {
throw new ReactContextError('AnnouncementBarProvider');
}
return api;
}