@@ -60,184 +60,187 @@ function SimpleModal({ isOpen, onClose, children }: SimpleModalProps) {
6060 ) ;
6161}
6262
63- interface RootProps {
64- children : React . ReactNode ;
65- disclaimerContent ?: React . ReactNode | string ;
66- showDisclaimer ?: boolean ;
67- }
68-
69- export default function Root ( {
70- children,
71- disclaimerContent = "This handbook is not intended to replace official documentation. This is internal material used for onboarding new team members. We open source it in the hopes that it helps somebody else, but beware it can be outdated on the latest updates." ,
72- showDisclaimer = false ,
73- } : RootProps ) {
74- const [ showModal , setShowModal ] = useState ( false ) ;
75-
76- const handleCloseModal = useCallback ( ( ) => setShowModal ( false ) , [ ] ) ;
77-
78- useEffect ( ( ) => {
79- if ( ! showDisclaimer ) return ;
63+ /**
64+ * Manages the disclaimer button event listener setup and cleanup
65+ * This function is extracted to prevent multiple event listener attachments
66+ * during Docusaurus navigation
67+ */
68+ function createDisclaimerButtonManager ( onDisclaimerClick : ( ) => void ) {
69+ let isListenerAttached = false ;
70+ let pollInterval : NodeJS . Timeout | undefined ;
71+ let currentButton : HTMLElement | null = null ;
72+ let mutationObserver : MutationObserver | null = null ;
73+
74+ const attachListener = ( ) => {
75+ const disclaimerBtn = document . getElementById ( "disclaimer-btn" ) ;
76+
77+ // Check if this is a different button or if we need to reattach
78+ if (
79+ disclaimerBtn &&
80+ ( disclaimerBtn !== currentButton || ! isListenerAttached )
81+ ) {
82+ // Remove listener from old button if it exists
83+ if ( currentButton && isListenerAttached ) {
84+ currentButton . removeEventListener ( "click" , onDisclaimerClick ) ;
85+ }
8086
81- const handleNavbarClick = ( ) => {
82- setShowModal ( true ) ;
83- } ;
87+ disclaimerBtn . addEventListener ( "click" , onDisclaimerClick ) ;
88+ currentButton = disclaimerBtn ;
89+ isListenerAttached = true ;
8490
85- let isListenerAttached = false ;
86- let pollInterval : NodeJS . Timeout | undefined ;
87- let currentButton : HTMLElement | null = null ;
88- let mutationObserver : MutationObserver | null = null ;
89-
90- const attachListener = ( ) => {
91- const disclaimerBtn = document . getElementById ( "disclaimer-btn" ) ;
92-
93- // Check if this is a different button or if we need to reattach
94- if (
95- disclaimerBtn &&
96- ( disclaimerBtn !== currentButton || ! isListenerAttached )
97- ) {
98- // Remove listener from old button if it exists
99- if ( currentButton && isListenerAttached ) {
100- currentButton . removeEventListener ( "click" , handleNavbarClick ) ;
101- }
91+ if ( pollInterval ) {
92+ clearInterval ( pollInterval ) ;
93+ pollInterval = undefined ;
94+ }
95+ return true ;
96+ }
97+ return false ;
98+ } ;
10299
103- disclaimerBtn . addEventListener ( "click" , handleNavbarClick ) ;
104- currentButton = disclaimerBtn ;
105- isListenerAttached = true ;
100+ const resetAndReattach = ( ) => {
101+ isListenerAttached = false ;
102+ currentButton = null ;
106103
107- if ( pollInterval ) {
108- clearInterval ( pollInterval ) ;
109- pollInterval = undefined ;
110- }
111- return true ;
104+ // Try to attach immediately
105+ if ( ! attachListener ( ) ) {
106+ // If button not found, start polling
107+ if ( ! pollInterval ) {
108+ pollInterval = setInterval ( ( ) => {
109+ attachListener ( ) ;
110+ } , 100 ) ;
112111 }
113- return false ;
114- } ;
112+ }
113+ } ;
115114
116- const resetAndReattach = ( ) => {
117- isListenerAttached = false ;
118- currentButton = null ;
119-
120- // Try to attach immediately
121- if ( ! attachListener ( ) ) {
122- // If button not found, start polling
123- if ( ! pollInterval ) {
124- pollInterval = setInterval ( ( ) => {
125- attachListener ( ) ;
126- } , 100 ) ;
127- }
128- }
129- } ;
115+ const setup = ( ) => {
116+ if ( typeof window === "undefined" ) return ( ) => { } ;
130117
131118 // Try to attach immediately
132119 resetAndReattach ( ) ;
133120
134121 // Set up MutationObserver to watch for DOM changes
135- if ( typeof window !== "undefined" ) {
136- mutationObserver = new MutationObserver ( ( mutations ) => {
137- let shouldReattach = false ;
138-
139- mutations . forEach ( ( mutation ) => {
140- // Check if nodes were added or removed
141- if ( mutation . type === "childList" ) {
142- // Check if our current button is still in the DOM
143- if ( currentButton && ! document . contains ( currentButton ) ) {
144- shouldReattach = true ;
145- }
146- // Check if a new disclaimer button was added
147- mutation . addedNodes . forEach ( ( node ) => {
148- if ( node . nodeType === Node . ELEMENT_NODE ) {
149- const element = node as Element ;
150- if (
151- element . id === "disclaimer-btn" ||
152- element . querySelector ( "#disclaimer-btn" )
153- ) {
154- shouldReattach = true ;
155- }
156- }
157- } ) ;
122+ mutationObserver = new MutationObserver ( ( mutations ) => {
123+ let shouldReattach = false ;
124+
125+ mutations . forEach ( ( mutation ) => {
126+ // Check if nodes were added or removed
127+ if ( mutation . type === "childList" ) {
128+ // Check if our current button is still in the DOM
129+ if ( currentButton && ! document . contains ( currentButton ) ) {
130+ shouldReattach = true ;
158131 }
159- } ) ;
160-
161- if ( shouldReattach ) {
162- resetAndReattach ( ) ;
132+ // Check if a new disclaimer button was added
133+ mutation . addedNodes . forEach ( ( node ) => {
134+ if ( node . nodeType === Node . ELEMENT_NODE ) {
135+ const element = node as Element ;
136+ if (
137+ element . id === "disclaimer-btn" ||
138+ element . querySelector ( "#disclaimer-btn" )
139+ ) {
140+ shouldReattach = true ;
141+ }
142+ }
143+ } ) ;
163144 }
164145 } ) ;
165146
166- // Start observing the navbar area specifically
167- const navbar = document . querySelector (
168- "[role='banner'], .navbar, .navbar__inner"
169- ) ;
170- if ( navbar ) {
171- mutationObserver . observe ( navbar , {
172- childList : true ,
173- subtree : true ,
174- } ) ;
175- } else {
176- // Fallback to observing the entire body
177- mutationObserver . observe ( document . body , {
178- childList : true ,
179- subtree : true ,
180- } ) ;
147+ if ( shouldReattach ) {
148+ resetAndReattach ( ) ;
181149 }
150+ } ) ;
151+
152+ // Start observing the navbar area specifically
153+ const navbar = document . querySelector (
154+ "[role='banner'], .navbar, .navbar__inner"
155+ ) ;
156+ if ( navbar ) {
157+ mutationObserver . observe ( navbar , {
158+ childList : true ,
159+ subtree : true ,
160+ } ) ;
161+ } else {
162+ // Fallback to observing the entire body
163+ mutationObserver . observe ( document . body , {
164+ childList : true ,
165+ subtree : true ,
166+ } ) ;
167+ }
182168
183- // Also listen for route change events specific to Docusaurus
184- const handleDocusaurusRouteChange = ( ) => {
185- setTimeout ( resetAndReattach , 100 ) ;
186- } ;
187-
188- // Listen for various navigation events
189- window . addEventListener ( "popstate" , handleDocusaurusRouteChange ) ;
190-
191- // Listen for custom events that might be fired by Docusaurus
192- window . addEventListener ( "routeUpdate" , handleDocusaurusRouteChange ) ;
193-
194- // Intercept history methods for programmatic navigation
195- const originalPushState = history . pushState ;
196- const originalReplaceState = history . replaceState ;
197-
198- history . pushState = function ( ...args ) {
199- originalPushState . apply ( history , args ) ;
200- handleDocusaurusRouteChange ( ) ;
201- } ;
202-
203- history . replaceState = function ( ...args ) {
204- originalReplaceState . apply ( history , args ) ;
205- handleDocusaurusRouteChange ( ) ;
206- } ;
207-
208- // Cleanup function
209- return ( ) => {
210- if ( pollInterval ) {
211- clearInterval ( pollInterval ) ;
212- }
169+ // Also listen for route change events specific to Docusaurus
170+ const handleDocusaurusRouteChange = ( ) => {
171+ setTimeout ( resetAndReattach , 100 ) ;
172+ } ;
213173
214- if ( currentButton && isListenerAttached ) {
215- currentButton . removeEventListener ( "click ", handleNavbarClick ) ;
216- }
174+ // Listen for various navigation events
175+ window . addEventListener ( "popstate ", handleDocusaurusRouteChange ) ;
176+ window . addEventListener ( "routeUpdate" , handleDocusaurusRouteChange ) ;
217177
218- if ( mutationObserver ) {
219- mutationObserver . disconnect ( ) ;
220- }
178+ // Intercept history methods for programmatic navigation
179+ const originalPushState = history . pushState ;
180+ const originalReplaceState = history . replaceState ;
221181
222- window . removeEventListener ( "popstate" , handleDocusaurusRouteChange ) ;
223- window . removeEventListener ( "routeUpdate" , handleDocusaurusRouteChange ) ;
182+ history . pushState = function ( ...args ) {
183+ originalPushState . apply ( history , args ) ;
184+ handleDocusaurusRouteChange ( ) ;
185+ } ;
224186
225- // Restore original methods
226- history . pushState = originalPushState ;
227- history . replaceState = originalReplaceState ;
228- } ;
229- }
187+ history . replaceState = function ( ...args ) {
188+ originalReplaceState . apply ( history , args ) ;
189+ handleDocusaurusRouteChange ( ) ;
190+ } ;
230191
231- // Fallback cleanup for server-side rendering
192+ // Return cleanup function
232193 return ( ) => {
233194 if ( pollInterval ) {
234195 clearInterval ( pollInterval ) ;
235196 }
197+
236198 if ( currentButton && isListenerAttached ) {
237- currentButton . removeEventListener ( "click" , handleNavbarClick ) ;
199+ currentButton . removeEventListener ( "click" , onDisclaimerClick ) ;
200+ }
201+
202+ if ( mutationObserver ) {
203+ mutationObserver . disconnect ( ) ;
238204 }
205+
206+ window . removeEventListener ( "popstate" , handleDocusaurusRouteChange ) ;
207+ window . removeEventListener ( "routeUpdate" , handleDocusaurusRouteChange ) ;
208+
209+ // Restore original methods
210+ history . pushState = originalPushState ;
211+ history . replaceState = originalReplaceState ;
239212 } ;
240- } , [ showDisclaimer ] ) ;
213+ } ;
214+
215+ return { setup } ;
216+ }
217+
218+ interface RootProps {
219+ children : React . ReactNode ;
220+ disclaimerContent ?: React . ReactNode | string ;
221+ showDisclaimer ?: boolean ;
222+ }
223+
224+ export default function Root ( {
225+ children,
226+ disclaimerContent = "This handbook is not intended to replace official documentation. This is internal material used for onboarding new team members. We open source it in the hopes that it helps somebody else, but beware it can be outdated on the latest updates." ,
227+ showDisclaimer = false ,
228+ } : RootProps ) {
229+ const [ showModal , setShowModal ] = useState ( false ) ;
230+
231+ const handleCloseModal = useCallback ( ( ) => setShowModal ( false ) , [ ] ) ;
232+ const handleDisclaimerClick = useCallback ( ( ) => setShowModal ( true ) , [ ] ) ;
233+
234+ useEffect ( ( ) => {
235+ if ( ! showDisclaimer ) return ;
236+
237+ const disclaimerManager = createDisclaimerButtonManager (
238+ handleDisclaimerClick
239+ ) ;
240+ const cleanup = disclaimerManager . setup ( ) ;
241+
242+ return cleanup ;
243+ } , [ showDisclaimer , handleDisclaimerClick ] ) ;
241244
242245 return (
243246 < >
0 commit comments