diff --git a/packages/docusaurus-theme-classic/src/__tests__/inlineScripts.test.ts b/packages/docusaurus-theme-classic/src/__tests__/inlineScripts.test.ts index 2690e0b09c89..f02d75fa4d1b 100644 --- a/packages/docusaurus-theme-classic/src/__tests__/inlineScripts.test.ts +++ b/packages/docusaurus-theme-classic/src/__tests__/inlineScripts.test.ts @@ -6,7 +6,10 @@ */ import {describe, expect, it} from 'vitest'; -import {getThemeInlineScript} from '../inlineScripts'; +import { + getThemeInlineScript, + getAnnouncementBarInlineScript, +} from '../inlineScripts'; describe('inlineScripts', () => { it('inline javascript for default options', () => { @@ -40,4 +43,73 @@ describe('inlineScripts', () => { }), ).toMatchSnapshot(); }); + + describe('getAnnouncementBarInlineScript', () => { + it('generates inline script with default announcement id and namespace', () => { + const script = getAnnouncementBarInlineScript({ + siteStorage: { + type: 'localStorage', + namespace: '', + }, + }); + expect(script).toContain("localStorage.getItem('docusaurus.announcement.id') !== \"announcement-bar\""); + expect(script).toContain("localStorage.getItem('docusaurus.announcement.dismiss') === 'true'"); + expect(script).toContain("document.documentElement.setAttribute('data-announcement-bar-initially-dismissed', isDismissed())"); + }); + + it('generates inline script with custom announcement id and custom namespace', () => { + const script = getAnnouncementBarInlineScript({ + siteStorage: { + type: 'localStorage', + namespace: '.custom-namespace', + }, + announcementBar: { + id: 'announcement-v2', + }, + }); + expect(script).toContain("localStorage.getItem('docusaurus.announcement.id.custom-namespace') !== \"announcement-v2\""); + expect(script).toContain("localStorage.getItem('docusaurus.announcement.dismiss.custom-namespace') === 'true'"); + }); + + it('evaluates to false when a new announcement id differs from dismissed one', () => { + const script = getAnnouncementBarInlineScript({ + siteStorage: { + type: 'localStorage', + namespace: '', + }, + announcementBar: { + id: 'announcement-v2', + }, + }); + + const store = new Map(); + const mockLocalStorage = { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + }; + let attributeValue = ''; + const mockDocument = { + documentElement: { + setAttribute: (_name: string, value: string | boolean) => { + attributeValue = String(value); + }, + }, + }; + + const fn = new Function('localStorage', 'document', script); + + // Simulate a user who dismissed announcement-v1 + mockLocalStorage.setItem('docusaurus.announcement.id', 'announcement-v1'); + mockLocalStorage.setItem('docusaurus.announcement.dismiss', 'true'); + + fn(mockLocalStorage, mockDocument); + expect(attributeValue).toBe('false'); + + // Simulate when user has dismissed current announcement-v2 + mockLocalStorage.setItem('docusaurus.announcement.id', 'announcement-v2'); + + fn(mockLocalStorage, mockDocument); + expect(attributeValue).toBe('true'); + }); + }); }); diff --git a/packages/docusaurus-theme-classic/src/index.ts b/packages/docusaurus-theme-classic/src/index.ts index 8c5a236835a3..b57e3bf69eb1 100644 --- a/packages/docusaurus-theme-classic/src/index.ts +++ b/packages/docusaurus-theme-classic/src/index.ts @@ -134,7 +134,7 @@ export default function themeClassic( innerHTML: ` ${getThemeInlineScript({colorMode, siteStorage})} ${DataAttributeQueryStringInlineJavaScript} -${announcementBar ? getAnnouncementBarInlineScript({siteStorage}) : ''} +${announcementBar ? getAnnouncementBarInlineScript({siteStorage, announcementBar}) : ''} `, }, ], diff --git a/packages/docusaurus-theme-classic/src/inlineScripts.ts b/packages/docusaurus-theme-classic/src/inlineScripts.ts index fe49094a52c4..30458feeca89 100644 --- a/packages/docusaurus-theme-classic/src/inlineScripts.ts +++ b/packages/docusaurus-theme-classic/src/inlineScripts.ts @@ -76,21 +76,28 @@ export const DataAttributeQueryStringInlineJavaScript = ` // the announcement bar asap (before React hydration) export function getAnnouncementBarInlineScript({ siteStorage, + announcementBar, }: { siteStorage: SiteStorage; + announcementBar?: {id?: string}; }): string { // Duplicated constant. Unfortunately we can't import it from theme-common, as // we need to support older nodejs versions without ESM support // TODO: import from theme-common once we support Node.js with ESM support // + move all those announcementBar stuff there too const AnnouncementBarDismissStorageKey = `docusaurus.announcement.dismiss${siteStorage.namespace}`; + const AnnouncementBarIdStorageKey = `docusaurus.announcement.id${siteStorage.namespace}`; const AnnouncementBarDismissDataAttribute = 'data-announcement-bar-initially-dismissed'; + const id = announcementBar?.id ?? 'announcement-bar'; /* language=js */ return `(function() { function isDismissed() { try { + if (localStorage.getItem('${AnnouncementBarIdStorageKey}') !== ${JSON.stringify(id)}) { + return false; + } return localStorage.getItem('${AnnouncementBarDismissStorageKey}') === 'true'; } catch (err) {} return false; diff --git a/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx b/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx index 71546d20d2b4..d42ea537ef4b 100644 --- a/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx +++ b/packages/docusaurus-theme-common/src/contexts/announcementBar.tsx @@ -25,7 +25,8 @@ const AnnouncementBarDismissStorage = createStorageSlot( ); const IdStorage = createStorageSlot('docusaurus.announcement.id'); -const isDismissedInStorage = () => +const isDismissedInStorage = (id?: string) => + (!id || IdStorage.get() === id) && AnnouncementBarDismissStorage.get() === 'true'; const setDismissedInStorage = (bool: boolean) => AnnouncementBarDismissStorage.set(String(bool)); @@ -48,14 +49,14 @@ function useContextValue(): ContextValue { const [isClosed, setClosed] = useState(() => isBrowser ? // On client navigation: init with local storage value - isDismissedInStorage() + isDismissedInStorage(announcementBar?.id) : // On server/hydration: always visible to prevent layout shifts (will be hidden with css if needed) false, ); // Update state after hydration useEffect(() => { - setClosed(isDismissedInStorage()); - }, []); + setClosed(isDismissedInStorage(announcementBar?.id)); + }, [announcementBar?.id]); const handleClose = useCallback(() => { setDismissedInStorage(true); @@ -77,7 +78,7 @@ function useContextValue(): ContextValue { setDismissedInStorage(false); } - if (isNewAnnouncement || !isDismissedInStorage()) { + if (isNewAnnouncement || !isDismissedInStorage(id)) { setClosed(false); } }, [announcementBar]);