From 3c57c7b358e87a43282f546610d3fb54541d68f4 Mon Sep 17 00:00:00 2001 From: Abdul karim mia <97598943+abdul-karim-mia@users.noreply.github.com> Date: Mon, 9 Nov 2026 04:57:15 +0530 Subject: [PATCH] fix(theme): show new announcement bar immediately when id changes When an announcement bar was dismissed and a site subsequently ships a new announcement with a different id, the new announcement bar stayed hidden on first page load. The inline script before hydration evaluated docusaurus.announcement.dismiss without comparing the stored announcement id against the current one, setting data-announcement-bar-initially-dismissed='true' and hiding the bar via CSS until a subsequent reload. Pass announcementBar configuration to getAnnouncementBarInlineScript to verify that the stored announcement id matches before treating it as dismissed, and ensure isDismissedInStorage in AnnouncementBarContext also validates the announcement id. Fixes #12646 Signed-off-by: Abdul karim mia <97598943+abdul-karim-mia@users.noreply.github.com> --- .../src/__tests__/inlineScripts.test.ts | 74 ++++++++++++++++++- .../docusaurus-theme-classic/src/index.ts | 2 +- .../src/inlineScripts.ts | 7 ++ .../src/contexts/announcementBar.tsx | 11 +-- 4 files changed, 87 insertions(+), 7 deletions(-) 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]);