Skip to content

Commit 3bb093b

Browse files
committed
fix: prevent multiple event listeners
1 parent 569d975 commit 3bb093b

1 file changed

Lines changed: 152 additions & 149 deletions

File tree

  • packages/common-config/src/components/theme/Root

packages/common-config/src/components/theme/Root/index.tsx

Lines changed: 152 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)