Skip to content

Enhance modal copy functionality with improved event cleanup and lifecycle management#41

Open
Aarya-Chaudhari wants to merge 14 commits intoAOSSIE-Org:mainfrom
Aarya-Chaudhari:js/copy
Open

Enhance modal copy functionality with improved event cleanup and lifecycle management#41
Aarya-Chaudhari wants to merge 14 commits intoAOSSIE-Org:mainfrom
Aarya-Chaudhari:js/copy

Conversation

@Aarya-Chaudhari
Copy link
Contributor

@Aarya-Chaudhari Aarya-Chaudhari commented Mar 3, 2026

Additional Notes:

This PR enhances the modal copy functionality in src/social-share-button.js with improved reliability and better lifecycle management.

Changes Made

  • Added isCopying guard to prevent duplicate copy actions
  • Improved clipboard handling with secure-context check
  • Simplified fallback copy method
  • Removed all console.error from production code
  • Refactored event handling to:
    • Store handler references
    • Prevent duplicate event attachment
    • Properly clean up listeners in destroy()
  • Added ownsButton flag to ensure safe DOM cleanup

Why This Change?

Previously:

  • Event listeners were not explicitly removed
  • Copy action could be triggered multiple times rapidly
  • Console errors existed in production build
  • Component teardown was incomplete

Now:

  • Events are safely attached and detached
  • No memory leak risk
  • No duplicate listeners
  • No production console noise
  • Full component lifecycle cleanup

Checklist

  • My PR addresses a single issue, fixes a single bug or makes a single improvement.
  • My code follows the project's code style and conventions
  • If applicable, I have made corresponding changes or additions to the documentation
  • If applicable, I have made corresponding changes or additions to tests
  • My changes generate no new warnings or errors
  • I have joined the Discord server and I will share a link to this PR with the project maintainers there
  • I have read the Contribution Guidelines
  • Once I submit my PR, CodeRabbit AI will automatically review it and I will address CodeRabbit's comments.

AI Usage Disclosure

Check one of the checkboxes below:

  • This PR does not contain AI-generated code at all.
  • This PR contains AI-generated code. I have tested the code locally and I am responsible for it.

I have used the following AI models and tools: TODO

⚠️ AI Notice - Important!

We encourage contributors to use AI tools responsibly when creating Pull Requests. While AI can be a valuable aid, it is essential to ensure that your contributions meet the task requirements, build successfully, include relevant tests, and pass all linters. Submissions that do not meet these standards may be closed without warning to maintain the quality and integrity of the project. Please take the time to understand the changes you are proposing and their impact.

Summary by CodeRabbit

  • New Features

    • Copy-to-clipboard buttons added to all code blocks with brief "Copied!" feedback and accessible status announcements.
  • Bug Fixes

    • More reliable clipboard handling with a robust fallback and fewer duplicate event attachments.
    • Improved cleanup to prevent lingering handlers and memory leaks.
  • Improvements

    • Stabilized modal, overlay, and per-instance state management; better handling of dynamic URLs and custom color updates.

@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

📝 Walkthrough

Walkthrough

Added copy-to-clipboard UI and behavior for code blocks in index.html. Refactored src/social-share-button.js to track per-instance handler state, improve event binding/unbinding, enhance copy logic with clipboard API fallback, and ensure proper cleanup and ownership tracking.

Changes

Cohort / File(s) Summary
Copy-to-Clipboard Code Blocks
index.html
Inserted .copy-btn and .copy-status elements into code block markup, added styles and aria-live status, and wired JavaScript to copy code text, show "Copied!" feedback, update status span, and revert after 2s.
Social share: event & state refactor
src/social-share-button.js
Added per-instance state fields (e.g., ownsButton, eventsAttached, isCopying), replaced inline handlers with bound instance handlers, rewrote attachEvents/destroy for comprehensive listener management, improved copyLink to use navigator.clipboard with a fallback and unified reset logic, and updated updateOptions/applyCustomColors to avoid cross-instance leaks.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant Btn as Share Button
  participant Modal as Share Modal
  participant Input as URL Input
  participant Clipboard as Clipboard API
  rect rgba(200,200,255,0.5)
    User->>Btn: click
    Btn->>Modal: open modal (attach handlers)
  end
  rect rgba(200,255,200,0.5)
    User->>Modal: click copy
    Modal->>Input: read URL
    Modal->>Clipboard: navigator.clipboard.writeText(url)
    Clipboard-->>Modal: success / failure
    Modal->>Modal: update UI ("Copied!" state), set timeout to reset
  end
  rect rgba(255,200,200,0.5)
    User->>Modal: close / ESC
    Modal->>Btn: remove bindings (destroy) if owned
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • Social share button #1: Refactors the same SocialShareButton implementation and previously introduced the class that this PR further modifies.

Poem

🐰
I hopped to find a button bright,
Pressed it once — the code took flight.
Handlers neat and clipboard kind,
No stray listeners left behind.
Hooray — a tidy sharing night!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main changes: enhanced modal copy functionality with improved event cleanup and lifecycle management, which directly aligns with the substantial refactoring in src/social-share-button.js.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/social-share-button.js (1)

397-400: Release retained references in destroy() after teardown.

After removing listeners and nodes, handler and element references are still kept on the instance. Clearing them helps GC and avoids accidental reuse after destroy.

Suggested refactor
   document.body.style.overflow = '';
   this.eventsAttached = false;
   this.isCopying = false;
+  this.platformBtnHandlers = [];
+  this.handleButtonClick = null;
+  this.handleOverlayClick = null;
+  this.handleCloseClick = null;
+  this.handleCopyClick = null;
+  this.handleInputClick = null;
+  this.handleKeydown = null;
+  this.customColorMouseEnterHandler = null;
+  this.customColorMouseLeaveHandler = null;
+  this.modal = null;
+  this.button = null;
+  this.ownsButton = false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/social-share-button.js` around lines 397 - 400, The destroy() method
currently resets document.body.style.overflow and toggles this.eventsAttached
and this.isCopying but retains DOM and handler references; update destroy() to
null out all instance-held DOM elements and event handler references (for
example this.shareButton, this.shareMenu, this.tooltip, this.handleClick,
this.handleOutsideClick or any bound handlers you created) and reset state flags
(this.eventsAttached = false, this.isCopying = false) so the instance no longer
holds references that prevent GC or allow accidental reuse after teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@index.html`:
- Around line 392-393: The markup nests a second <code> tag around the import
statement, creating invalid HTML; fix by removing the inner/outer duplication so
the import line "import SocialShareButton from 'social-share-button';" is
enclosed by a single <code> element (or close the outer <code> before the
import), ensuring only one <code> wraps the import and no nested <code> elements
remain.
- Around line 576-604: The copy button handler (copyButtons.forEach ->
button.addEventListener) must guard against missing/unsupported Clipboard API
and handle writeText failures: check for navigator.clipboard and typeof
navigator.clipboard.writeText === 'function' before calling it, and add a .catch
handler to set the copy-status aria-live text to an error message and restore
button text; if unavailable, fall back to a safe legacy copy method (create a
hidden textarea, select and document.execCommand('copy'), then remove it) and
similarly update statusSpan and button text on success or failure. Ensure all
branches restore the original button.textContent and clear/update statusSpan
(class "copy-status") appropriately.

In `@src/social-share-button.js`:
- Around line 326-347: The copy flow currently accesses navigator, window, and
document directly causing SSR failures; wrap those checks with typeof guards
before using them: in the block that calls navigator.clipboard and
window.isSecureContext, first ensure typeof navigator !== 'undefined' && typeof
window !== 'undefined' and only then call
navigator.clipboard.writeText(this.options.url), and in fallbackCopy(input)
ensure typeof document !== 'undefined' before calling document.execCommand,
returning false if document is unavailable; keep behavior of reset(...) and
fallbackCopy(...) unchanged otherwise.

---

Nitpick comments:
In `@src/social-share-button.js`:
- Around line 397-400: The destroy() method currently resets
document.body.style.overflow and toggles this.eventsAttached and this.isCopying
but retains DOM and handler references; update destroy() to null out all
instance-held DOM elements and event handler references (for example
this.shareButton, this.shareMenu, this.tooltip, this.handleClick,
this.handleOutsideClick or any bound handlers you created) and reset state flags
(this.eventsAttached = false, this.isCopying = false) so the instance no longer
holds references that prevent GC or allow accidental reuse after teardown.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d73852d and a9da158.

📒 Files selected for processing (2)
  • index.html
  • src/social-share-button.js

Comment on lines +576 to +604
const copyButtons = document.querySelectorAll('.copy-btn');

copyButtons.forEach((button) => {
button.addEventListener('click', () => {
const codeBlock = button.closest('.code-block')?.querySelector('code');
const text = codeBlock ? codeBlock.innerText : '';

navigator.clipboard.writeText(text).then(() => {
const originalText = button.textContent;

// Find aria-live span
const statusSpan = button.nextElementSibling;

if (statusSpan && statusSpan.classList.contains("copy-status")) {
statusSpan.textContent = "Code copied to clipboard";
}

button.textContent = "Copied!";

setTimeout(() => {
button.textContent = originalText;

if (statusSpan && statusSpan.classList.contains("copy-status")) {
statusSpan.textContent = "";
}
}, 2000);
});
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

navigator.clipboard.writeText secure context requirements fallback

💡 Result:

navigator.clipboard.writeText() — secure-context requirements

1) Must be a secure context

  • The Async Clipboard API (including navigator.clipboard.writeText) is only available in secure contexts (typically HTTPS). (developer.mozilla.org)
  • Browsers also treat some local/dev origins as “potentially trustworthy” (commonly http://localhost, http://127.0.0.1, and often file://). (developer.mozilla.org)
  • Practical symptom: on non-secure pages, navigator.clipboard may be missing/undefined, or calls reject. (webkit.org)

2) User-gesture / transient activation is often required

  • Many browsers require the call to happen in a user gesture (e.g., inside a click handler) or it will be rejected. (webkit.org)

3) Permissions differ by browser

  • Spec-wise, writing expects Permissions API clipboard-write, but real behavior varies:
    • Chromium: write requires either clipboard-write permission or transient activation; if permission is granted it can persist. (developer.mozilla.org)
    • Firefox/Safari: generally require transient activation; clipboard-write permission via Permissions API isn’t supported the same way. (developer.mozilla.org)

4) If inside an <iframe>

  • Clipboard access from iframes may require the appropriate Permissions-Policy allowances (notably in Chromium). (developer.mozilla.org)

Fallback when writeText() isn’t available (or fails)

Use the legacy (deprecated) document.execCommand('copy') approach:

async function copyText(text) {
  // Prefer async clipboard when available
  if (window.isSecureContext && navigator.clipboard?.writeText) {
    return navigator.clipboard.writeText(text);
  }

  // Fallback: execCommand('copy')
  const ta = document.createElement('textarea');
  ta.value = text;
  ta.setAttribute('readonly', '');
  ta.style.position = 'absolute';
  ta.style.left = '-9999px';
  document.body.appendChild(ta);
  ta.select();
  const ok = document.execCommand('copy');
  document.body.removeChild(ta);

  if (!ok) throw new Error('Copy failed');
}

Notes:

  • writeText() is secure-context-only. (developer.mozilla.org)
  • The execCommand('copy') pattern is commonly used as a compatibility fallback (despite being deprecated). (copypasterun.com)

Citations:


🌐 Web query:

document.execCommand copy fallback clipboard compatibility

💡 Result:

Use the Async Clipboard API first (navigator.clipboard.writeText()), and fall back to document.execCommand('copy') only when needed (it’s deprecated and may stop working in the future). [1][2]

export async function copyToClipboard(text) {
  // Preferred: Async Clipboard API (secure context + usually a user gesture)
  if (navigator.clipboard?.writeText) {
    try {
      await navigator.clipboard.writeText(text);
      return true;
    } catch (_) {
      // fall through to execCommand fallback
    }
  }

  // Fallback: execCommand('copy') using a temporary textarea + selection
  const ta = document.createElement("textarea");
  ta.value = text;

  // keep it off-screen but selectable
  ta.style.position = "fixed";
  ta.style.top = "-9999px";
  ta.style.left = "-9999px";

  document.body.appendChild(ta);
  ta.focus();
  ta.select();

  let ok = false;
  try {
    ok = document.execCommand("copy");
  } catch (_) {
    ok = false;
  } finally {
    document.body.removeChild(ta);
  }
  return ok;
}

Compatibility notes (high-signal):

  • navigator.clipboard is the recommended API, but writing commonly requires “transient user activation” (a user gesture) and a secure context (HTTPS). [2]
  • document.execCommand('copy') typically only works when called from a short-lived user gesture handler (e.g., a click). [3]
  • Don’t rely on document.queryCommandSupported() for detection; it’s also deprecated. [1]

Sources: MDN on document.execCommand() deprecation and clipboard commands [1]; MDN Clipboard API security/activation requirements and browser behavior notes [2]; MDN (extensions) note about user-gesture requirement for execCommand('copy') [3].


🌐 Web query:

Clipboard API vs document.execCommand copy browser support 2025

💡 Result:

TL;DR (2025)

  • Use the Async Clipboard API (navigator.clipboard.writeText() / readText()) when possible: it’s the modern, standards-based approach, but requires HTTPS (“secure context”) and typically a user gesture / transient activation, with permission-model differences by browser. [1][2]
  • document.execCommand('copy') is deprecated and “may cease to work at any time,” but it remained very widely supported across browsers in 2025 largely for legacy compatibility. [3][4]

Browser support reality (as of 2025)

Async Clipboard API (recommended)

  • Supported in modern browsers (roughly): Chrome 76+ / Edge 79+ / Safari 13.1+ / Firefox 127+. [2]
  • Constraints/behavior differences:
    • Secure context required (HTTPS). [1][2]
    • Chrome/Chromium may use Permissions API (clipboard-read/clipboard-write) and has iframe policy controls. [2]
    • Firefox & Safari don’t support those Permissions API clipboard permissions (per MDN) and generally rely on transient activation (and for reads, a user-facing paste prompt flow). [2]

document.execCommand('copy') (legacy fallback)

  • Marked deprecated and not recommended for new work. [3]
  • Still showed broad support across major browsers (Chrome/Edge/Firefox/Safari) on compatibility tables, but behavior is inconsistent and can be restricted to user gestures/selections. [3][4]

Practical recommendation (2025)

  • Implement Clipboard API first, and keep execCommand('copy') only as a fallback for older/incompatible environments—especially if you must support older Firefox versions (pre-127) or unusual embedded browsers. [2][3]

Sources: MDN Clipboard API & security/permission notes [2], MDN Clipboard write secure-context note [1], MDN execCommand deprecation warning [3], Can I use execCommand support table [4].


Guard clipboard usage and add a failure path for unsupported/insecure contexts.

navigator.clipboard.writeText() requires a secure context (HTTPS); on non-secure contexts (http://, file://, or restricted browsers), it becomes unavailable or fails. The current code has no fallback and no error handling, causing silent failures.

Suggested fix
+const copyText = (text) => {
+  if (window.isSecureContext && navigator.clipboard?.writeText) {
+    return navigator.clipboard.writeText(text).then(() => true);
+  }
+
+  const textarea = document.createElement('textarea');
+  textarea.value = text;
+  textarea.setAttribute('readonly', '');
+  textarea.style.position = 'absolute';
+  textarea.style.left = '-9999px';
+  document.body.appendChild(textarea);
+  textarea.select();
+  const ok = document.execCommand('copy');
+  document.body.removeChild(textarea);
+  return Promise.resolve(ok);
+};
+
 copyButtons.forEach((button) => {
   button.addEventListener('click', () => {
     const codeBlock = button.closest('.code-block')?.querySelector('code');
-const text = codeBlock ? codeBlock.innerText : '';
+    const text = codeBlock ? codeBlock.innerText : '';
 
-    navigator.clipboard.writeText(text).then(() => {
+    copyText(text).then((ok) => {
+      if (!ok) return;
       const originalText = button.textContent;
 
       // Find aria-live span
       const statusSpan = button.nextElementSibling;
@@
-});
+    }).catch(() => {});
   });
 });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@index.html` around lines 576 - 604, The copy button handler
(copyButtons.forEach -> button.addEventListener) must guard against
missing/unsupported Clipboard API and handle writeText failures: check for
navigator.clipboard and typeof navigator.clipboard.writeText === 'function'
before calling it, and add a .catch handler to set the copy-status aria-live
text to an error message and restore button text; if unavailable, fall back to a
safe legacy copy method (create a hidden textarea, select and
document.execCommand('copy'), then remove it) and similarly update statusSpan
and button text on success or failure. Ensure all branches restore the original
button.textContent and clear/update statusSpan (class "copy-status")
appropriately.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/social-share-button.js`:
- Around line 371-408: The destroy() method currently removes event listeners
and DOM nodes but retains references (platformBtnHandlers array, this.modal,
this.button, and handler properties), risking detached-node retention; after
removing listeners and removing DOM nodes in destroy(), clear and null out
retained references: empty platformBtnHandlers (e.g., platformBtnHandlers.length
= 0), set this.modal = null, this.button = null, this.handleCopyClick = null,
this.handleInputClick = null, this.handleKeydown = null,
this.customColorMouseEnterHandler = null, this.customColorMouseLeaveHandler =
null, and also reset flags like this.ownsButton = false (if used),
this.eventsAttached = false, and this.isCopying = false so the instance no
longer holds DOM or handler references.
- Around line 327-338: The guard currently checks for navigator.clipboard but
not that writeText exists/callable, so calling
navigator.clipboard.writeText(this.options.url) can throw synchronously; modify
the block around navigator.clipboard.writeText to first verify typeof
navigator.clipboard.writeText === 'function' and then call it inside a
try/catch; on success call reset(true) in the try's resolved path, and in the
catch fall back to const success = this.fallbackCopy(input); reset(success) to
ensure isCopying is always cleared; keep references to
navigator.clipboard.writeText, this.options.url, this.fallbackCopy(input), and
reset(...) when implementing the change.
- Around line 357-387: The destroy() method currently touches DOM globals and
will throw in SSR; add a guard at the top of destroy() so it returns early when
not running in a browser (use this.isBrowser), and ensure any direct uses of
document or DOM queries (document.removeEventListener,
this.modal?.querySelector, etc.) are only executed when this.isBrowser is true;
update the destroy() function to check this.isBrowser before performing event
removal and querySelector operations to avoid ReferenceError during SSR.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a9da158 and d1df56d.

📒 Files selected for processing (2)
  • index.html
  • src/social-share-button.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • index.html

Comment on lines +327 to +338
if (
typeof window !== 'undefined' &&
typeof navigator !== 'undefined' &&
navigator.clipboard &&
window.isSecureContext
) {
navigator.clipboard.writeText(this.options.url)
.then(() => reset(true))
.catch(() => {
const success = this.fallbackCopy(input);
reset(success);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

ls -la src/social-share-button.js && wc -l src/social-share-button.js

Repository: AOSSIE-Org/SocialShareButton

Length of output: 179


🏁 Script executed:

cat -n src/social-share-button.js | sed -n '320,350p'

Repository: AOSSIE-Org/SocialShareButton

Length of output: 970


🏁 Script executed:

cat -n src/social-share-button.js | sed -n '310,345p'

Repository: AOSSIE-Org/SocialShareButton

Length of output: 1189


🏁 Script executed:

rg -B 5 "navigator.clipboard" src/social-share-button.js

Repository: AOSSIE-Org/SocialShareButton

Length of output: 261


🏁 Script executed:

cat -n src/social-share-button.js | sed -n '300,350p'

Repository: AOSSIE-Org/SocialShareButton

Length of output: 1639


🏁 Script executed:

rg "isCopying" src/social-share-button.js

Repository: AOSSIE-Org/SocialShareButton

Length of output: 206


🌐 Web query:

navigator.clipboard.writeText synchronous throw error MDN Clipboard API

💡 Result:

navigator.clipboard.writeText() is asynchronous: it returns a Promise, so failures usually show up as a Promise rejection, not as an immediate (“synchronous”) throw. MDN documents that writeText() returns a Promise and that failure can be a NotAllowedError (a DOMException) when writing isn’t allowed. (developer.mozilla.org)

Why you might think it “throws synchronously”:

  • You didn’t await (or attach .catch), so the error appears later as an unhandled promise rejection, and a surrounding non-await try/catch won’t catch it:

    try { navigator.clipboard.writeText("x"); } catch (e) {} // won't catch async rejection

    Correct:

    try { await navigator.clipboard.writeText("x"); } catch (e) {}
    // or: navigator.clipboard.writeText("x").catch(console.error);

    (developer.mozilla.org)

  • navigator.clipboard is missing/undefined, commonly because the API is only available in secure contexts (HTTPS; localhost is treated as secure for dev). In that case, you’ll get an immediate TypeError like “Cannot read properties of undefined (reading 'writeText')”. (developer.mozilla.org)

Common reasons the Promise is rejected (often as NotAllowedError):

  • Not a secure context (HTTPS requirement). (developer.mozilla.org)
  • Missing transient user activation (must be triggered by a user gesture like a click; this is explicitly required for writing in Firefox/Safari per MDN). (developer.mozilla.org)
  • In iframes, blocked by Permissions-Policy (clipboard-write). (developer.mozilla.org)

Citations:


Guard navigator.clipboard.writeText against partial implementations.

While the code checks for navigator.clipboard, it doesn't verify that writeText is callable. In browsers with partial Clipboard API implementations where navigator.clipboard exists but lacks the writeText method, calling it throws a synchronous TypeError before a Promise is created. This bypasses the .catch() handler and leaves isCopying stuck true.

Add a type check for navigator.clipboard.writeText and wrap the call in try-catch:

Proposed fix
+  const canUseClipboardApi =
+    typeof window !== 'undefined' &&
+    typeof navigator !== 'undefined' &&
+    !!navigator.clipboard &&
+    typeof navigator.clipboard.writeText === 'function' &&
+    window.isSecureContext;
+
+  if (canUseClipboardApi) {
+    try {
+      navigator.clipboard.writeText(this.options.url)
+        .then(() => reset(true))
+        .catch(() => {
+          const success = this.fallbackCopy(input);
+          reset(success);
+        });
+    } catch {
+      const success = this.fallbackCopy(input);
+      reset(success);
+    }
+  } else {
     const success = this.fallbackCopy(input);
     reset(success);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
typeof window !== 'undefined' &&
typeof navigator !== 'undefined' &&
navigator.clipboard &&
window.isSecureContext
) {
navigator.clipboard.writeText(this.options.url)
.then(() => reset(true))
.catch(() => {
const success = this.fallbackCopy(input);
reset(success);
});
const canUseClipboardApi =
typeof window !== 'undefined' &&
typeof navigator !== 'undefined' &&
!!navigator.clipboard &&
typeof navigator.clipboard.writeText === 'function' &&
window.isSecureContext;
if (canUseClipboardApi) {
try {
navigator.clipboard.writeText(this.options.url)
.then(() => reset(true))
.catch(() => {
const success = this.fallbackCopy(input);
reset(success);
});
} catch {
const success = this.fallbackCopy(input);
reset(success);
}
} else {
const success = this.fallbackCopy(input);
reset(success);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/social-share-button.js` around lines 327 - 338, The guard currently
checks for navigator.clipboard but not that writeText exists/callable, so
calling navigator.clipboard.writeText(this.options.url) can throw synchronously;
modify the block around navigator.clipboard.writeText to first verify typeof
navigator.clipboard.writeText === 'function' and then call it inside a
try/catch; on success call reset(true) in the try's resolved path, and in the
catch fall back to const success = this.fallbackCopy(input); reset(success) to
ensure isCopying is always cleared; keep references to
navigator.clipboard.writeText, this.options.url, this.fallbackCopy(input), and
reset(...) when implementing the change.

Comment on lines +357 to +387
destroy() {
if (this.button && this.handleButtonClick) {
this.button.removeEventListener('click', this.handleButtonClick);
}

destroy() {
if (this.button && this.customColorMouseEnterHandler) {
this.button.removeEventListener('mouseenter', this.customColorMouseEnterHandler);
this.customColorMouseEnterHandler = null;
}
if (this.button && this.customColorMouseLeaveHandler) {
this.button.removeEventListener('mouseleave', this.customColorMouseLeaveHandler);
this.customColorMouseLeaveHandler = null;
}
if (this.modal && this.handleOverlayClick) {
this.modal.removeEventListener('click', this.handleOverlayClick);
}

if (this.button && this.button.parentNode) {
this.button.parentNode.removeChild(this.button);
}
if (this.modal && this.modal.parentNode) {
this.modal.parentNode.removeChild(this.modal);
}
document.body.style.overflow = '';
const closeBtn = this.modal?.querySelector('.social-share-modal-close');
if (closeBtn && this.handleCloseClick) {
closeBtn.removeEventListener('click', this.handleCloseClick);
}

this.platformBtnHandlers.forEach(({ btn, handler }) => {
btn.removeEventListener('click', handler);
});

const copyBtn = this.modal?.querySelector('.social-share-copy-btn');
if (copyBtn && this.handleCopyClick) {
copyBtn.removeEventListener('click', this.handleCopyClick);
}

const input = this.modal?.querySelector('.social-share-link-input input');
if (input && this.handleInputClick) {
input.removeEventListener('click', this.handleInputClick);
}

if (this.handleKeydown) {
document.removeEventListener('keydown', this.handleKeydown);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /dev/null && pwd

Repository: AOSSIE-Org/SocialShareButton

Length of output: 121


🏁 Script executed:

git ls-files | grep -E "social-share-button\.js|\.js$" | head -20

Repository: AOSSIE-Org/SocialShareButton

Length of output: 98


🏁 Script executed:

cat -n src/social-share-button.js | sed -n '350,420p'

Repository: AOSSIE-Org/SocialShareButton

Length of output: 2600


🏁 Script executed:

rg "isBrowser" src/social-share-button.js -B 2 -A 2

Repository: AOSSIE-Org/SocialShareButton

Length of output: 277


🏁 Script executed:

rg "destroy\(\)" src/social-share-button.js -B 5 -A 50 | head -100

Repository: AOSSIE-Org/SocialShareButton

Length of output: 1720


🏁 Script executed:

rg "destroy" src/social-share-button.js -B 3 -A 3

Repository: AOSSIE-Org/SocialShareButton

Length of output: 211


🏁 Script executed:

rg "destroy\(" --type js | grep -v node_modules | head -20

Repository: AOSSIE-Org/SocialShareButton

Length of output: 186


🏁 Script executed:

cat -n src/social-share-button.js | head -100

Repository: AOSSIE-Org/SocialShareButton

Length of output: 4598


Guard destroy() against SSR execution contexts.

The method directly accesses document at lines 386 and 405 without checking the browser environment. When called during SSR (e.g., from React cleanup), this will throw a ReferenceError. Use the existing this.isBrowser guard consistently:

Proposed fix
 destroy() {
+  if (!this.isBrowser) {
+    this.eventsAttached = false;
+    this.isCopying = false;
+    return;
+  }
+
   if (this.button && this.handleButtonClick) {
     this.button.removeEventListener('click', this.handleButtonClick);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
destroy() {
if (this.button && this.handleButtonClick) {
this.button.removeEventListener('click', this.handleButtonClick);
}
destroy() {
if (this.button && this.customColorMouseEnterHandler) {
this.button.removeEventListener('mouseenter', this.customColorMouseEnterHandler);
this.customColorMouseEnterHandler = null;
}
if (this.button && this.customColorMouseLeaveHandler) {
this.button.removeEventListener('mouseleave', this.customColorMouseLeaveHandler);
this.customColorMouseLeaveHandler = null;
}
if (this.modal && this.handleOverlayClick) {
this.modal.removeEventListener('click', this.handleOverlayClick);
}
if (this.button && this.button.parentNode) {
this.button.parentNode.removeChild(this.button);
}
if (this.modal && this.modal.parentNode) {
this.modal.parentNode.removeChild(this.modal);
}
document.body.style.overflow = '';
const closeBtn = this.modal?.querySelector('.social-share-modal-close');
if (closeBtn && this.handleCloseClick) {
closeBtn.removeEventListener('click', this.handleCloseClick);
}
this.platformBtnHandlers.forEach(({ btn, handler }) => {
btn.removeEventListener('click', handler);
});
const copyBtn = this.modal?.querySelector('.social-share-copy-btn');
if (copyBtn && this.handleCopyClick) {
copyBtn.removeEventListener('click', this.handleCopyClick);
}
const input = this.modal?.querySelector('.social-share-link-input input');
if (input && this.handleInputClick) {
input.removeEventListener('click', this.handleInputClick);
}
if (this.handleKeydown) {
document.removeEventListener('keydown', this.handleKeydown);
}
destroy() {
if (!this.isBrowser) {
this.eventsAttached = false;
this.isCopying = false;
return;
}
if (this.button && this.handleButtonClick) {
this.button.removeEventListener('click', this.handleButtonClick);
}
if (this.modal && this.handleOverlayClick) {
this.modal.removeEventListener('click', this.handleOverlayClick);
}
const closeBtn = this.modal?.querySelector('.social-share-modal-close');
if (closeBtn && this.handleCloseClick) {
closeBtn.removeEventListener('click', this.handleCloseClick);
}
this.platformBtnHandlers.forEach(({ btn, handler }) => {
btn.removeEventListener('click', handler);
});
const copyBtn = this.modal?.querySelector('.social-share-copy-btn');
if (copyBtn && this.handleCopyClick) {
copyBtn.removeEventListener('click', this.handleCopyClick);
}
const input = this.modal?.querySelector('.social-share-link-input input');
if (input && this.handleInputClick) {
input.removeEventListener('click', this.handleInputClick);
}
if (this.handleKeydown) {
document.removeEventListener('keydown', this.handleKeydown);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/social-share-button.js` around lines 357 - 387, The destroy() method
currently touches DOM globals and will throw in SSR; add a guard at the top of
destroy() so it returns early when not running in a browser (use
this.isBrowser), and ensure any direct uses of document or DOM queries
(document.removeEventListener, this.modal?.querySelector, etc.) are only
executed when this.isBrowser is true; update the destroy() function to check
this.isBrowser before performing event removal and querySelector operations to
avoid ReferenceError during SSR.

Comment on lines +371 to +408
this.platformBtnHandlers.forEach(({ btn, handler }) => {
btn.removeEventListener('click', handler);
});

const copyBtn = this.modal?.querySelector('.social-share-copy-btn');
if (copyBtn && this.handleCopyClick) {
copyBtn.removeEventListener('click', this.handleCopyClick);
}

const input = this.modal?.querySelector('.social-share-link-input input');
if (input && this.handleInputClick) {
input.removeEventListener('click', this.handleInputClick);
}

if (this.handleKeydown) {
document.removeEventListener('keydown', this.handleKeydown);
}

if (this.button && this.customColorMouseEnterHandler) {
this.button.removeEventListener('mouseenter', this.customColorMouseEnterHandler);
}

if (this.button && this.customColorMouseLeaveHandler) {
this.button.removeEventListener('mouseleave', this.customColorMouseLeaveHandler);
}

if (this.ownsButton && this.button?.parentNode) {
this.button.parentNode.removeChild(this.button);
}

if (this.modal?.parentNode) {
this.modal.parentNode.removeChild(this.modal);
}

document.body.style.overflow = '';
this.eventsAttached = false;
this.isCopying = false;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Release retained instance references during teardown to avoid detached-node retention.

destroy() removes listeners but keeps platformBtnHandlers, element refs, and handler refs. If the instance remains referenced, detached DOM can stay retained.

Proposed fix
   if (this.modal?.parentNode) {
     this.modal.parentNode.removeChild(this.modal);
   }

   document.body.style.overflow = '';
+  this.platformBtnHandlers = [];
+  this.handleButtonClick = null;
+  this.handleOverlayClick = null;
+  this.handleCloseClick = null;
+  this.handleCopyClick = null;
+  this.handleInputClick = null;
+  this.handleKeydown = null;
+  this.customColorMouseEnterHandler = null;
+  this.customColorMouseLeaveHandler = null;
+  this.button = null;
+  this.modal = null;
+  this.ownsButton = false;
   this.eventsAttached = false;
   this.isCopying = false;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/social-share-button.js` around lines 371 - 408, The destroy() method
currently removes event listeners and DOM nodes but retains references
(platformBtnHandlers array, this.modal, this.button, and handler properties),
risking detached-node retention; after removing listeners and removing DOM nodes
in destroy(), clear and null out retained references: empty platformBtnHandlers
(e.g., platformBtnHandlers.length = 0), set this.modal = null, this.button =
null, this.handleCopyClick = null, this.handleInputClick = null,
this.handleKeydown = null, this.customColorMouseEnterHandler = null,
this.customColorMouseLeaveHandler = null, and also reset flags like
this.ownsButton = false (if used), this.eventsAttached = false, and
this.isCopying = false so the instance no longer holds DOM or handler
references.

@Aarya-Chaudhari
Copy link
Contributor Author

Hello @kpj2006
As suggested in PR #36, I’ve created this separate PR to address the library-related improvements.

This includes the modal copy enhancements and improved event lifecycle cleanup within src/social-share-button.js.

The previous PR now remains strictly scoped to the demo page changes, as requested.

Please review.

@kpj2006
Copy link
Contributor

kpj2006 commented Mar 4, 2026

@Aarya-Chaudhari pls consider some points:

  • follow pr template(mark x in satisfiable checkboxes otherwise you may close your pr).
  • have a look to all coderabbit reviews and try to answer them.
  • do not ask for assign on other issues until you have done work of one issue.
  • in assign, try to add what would be the proper flow of issue with manually review of LLMs response( do not directly cpy paste, some devs do that).
  • try to attach ss/video for what you have done in last change, it will help maintainer to review pr quickly.
  • do not tag maintainer until you have completed the work properly.

@kpj2006
Copy link
Contributor

kpj2006 commented Mar 4, 2026

@CodeRabbit according to last commit 4d7a14e review the changes in this pr.

@coderabbitai
Copy link

coderabbitai bot commented Mar 4, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the content of the main changed file
cat src/social-share-button.js

Length of output: 20225


@kpj2006 Here is a detailed review of the changes in commit 4d7a14e6:


Review of src/social-share-button.js

✅ What's Done Well

  • The isCopying guard correctly prevents duplicate copy triggers.
  • eventsAttached guard prevents duplicate listener attachment.
  • Per-instance handler storage (handleButtonClick, handleOverlayClick, etc.) is a solid pattern for clean removal.
  • destroy() now systematically removes all registered listeners.
  • ownsButton flag ensures only self-created buttons are removed from the DOM.
  • Secure-context check (window.isSecureContext) before using navigator.clipboard is correct.
  • fallbackCopy simplified to a boolean return is cleaner.

🔴 Critical Issues

1. Hardcoded Promotional/Marketing Text in getShareURL()

This is the most significant bug. The share messages for whatsapp, telegram, and email contain hardcoded phrases that are specific to the author's project and are not appropriate for a general-purpose library:

// WhatsApp
whatsappMessage = `🚀 ${title}...\n\nLive on the site 👀\nClean UI, smooth flow — worth peeking\n👇`;

// Telegram
telegramMessage = `🔗 ${title}...\n\nLive + working\nClean stuff, take a look 👇`;

// Email
emailBody = `Hey 👋\n\nSharing a clean project I came across:\n${title}...\n\nLive, simple, and usable — take a look 👇`;

Any user who imports this library and calls .share('whatsapp') would unknowingly send your project's promotional copy to their contacts. The messages should only use the user-supplied title, description, hashtags, and url fields:

// Suggested fix for WhatsApp
const whatsappMessage = `${title}${description ? '\n\n' + description : ''}${hashtagString ? '\n\n' + hashtagString : ''}\n\n${url}`;

🟠 Medium Issues

2. destroy() Does Not Null Out References — Potential Memory Leak

After cleanup, this.button, this.modal, and all handler references still hold values. This prevents garbage collection if the SocialShareButton instance is retained anywhere after destroy() is called.

// Add at the end of destroy()
this.button = null;
this.modal = null;
this.handleButtonClick = null;
this.handleOverlayClick = null;
this.handleCloseClick = null;
this.handleCopyClick = null;
this.handleInputClick = null;
this.handleKeydown = null;
this.platformBtnHandlers = [];
this.customColorMouseEnterHandler = null;
this.customColorMouseLeaveHandler = null;

3. destroy() Called While Copy Is In-Flight

If destroy() is called while isCopying is true (i.e., during the 2-second timeout window), the setTimeout callback will still fire and attempt to update copyBtn.textContent and this.isCopying — but the modal has already been removed from the DOM. The copyBtn reference inside the closure will point to a detached node.

Fix: Cancel the timeout in destroy():

// In copyLink(), store the timer:
this._copyTimer = setTimeout(() => {
  copyBtn.textContent = 'Copy';
  copyBtn.classList.remove('copied');
  this.isCopying = false;
  this._copyTimer = null;
}, 2000);

// In destroy(), clear it:
if (this._copyTimer) {
  clearTimeout(this._copyTimer);
  this._copyTimer = null;
  this.isCopying = false;
}

🟡 Minor Issues

4. Inconsistent Indentation in Constructor

The new state fields use a different indentation style (2-space) compared to the existing this.options block (4-space). This should be made uniform:

// Inconsistent — mix of 4-space (options block) and 2-space (new fields)
this.isModalOpen = false;
this.modal = null;         // 0-space (flush left)
this.ownsButton = false;   // 0-space

5. Redundant Environment Checks in copyLink()

The checks typeof window !== 'undefined' and typeof navigator !== 'undefined' inside copyLink() are redundant — copyLink() is only ever triggered by a button click event, which only fires in a browser. The this.isBrowser flag is already available:

// Simpler, consistent with the rest of the class:
if (this.isBrowser && navigator.clipboard && window.isSecureContext) {

6. eventsAttached Set Before Listeners Are Attached

In attachEvents(), this.eventsAttached = true is set on line 1, before any listeners are registered. If an exception is thrown mid-way (e.g., closeBtn is null), the flag will be true but the component will be in a partially initialized state. Consider setting it at the end of the method instead.


Summary Table

Severity Issue
🔴 Critical Hardcoded promotional text in getShareURL() for WhatsApp, Telegram, Email
🟠 Medium destroy() doesn't null handler/DOM references after cleanup
🟠 Medium In-flight copy setTimeout fires on detached DOM node after destroy()
🟡 Minor Inconsistent indentation in constructor
🟡 Minor Redundant typeof window/navigator checks in copyLink()
🟡 Minor eventsAttached = true set before listeners are registered

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants