-
-
Notifications
You must be signed in to change notification settings - Fork 41
Fix: improved UI and accessibility of social share button #128
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -241,6 +241,42 @@ class PostHogAdapter extends SocialShareAnalyticsPlugin { | |
| } | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Google Tag Manager (DataLayer) | ||
| // Requires: window.dataLayer exists. Uses dataLayer.push({ event, ...props }). | ||
| // Docs: https://developers.google.com/tag-manager/devguide | ||
| // ----------------------------------------------------------------------------- | ||
| class GoogleTagManagerAdapter extends SocialShareAnalyticsPlugin { | ||
| track(payload) { | ||
| if (typeof window === "undefined" || !Array.isArray(window.dataLayer)) { | ||
| return; | ||
| } | ||
| window.dataLayer.push({ | ||
| event: payload.eventName, | ||
| source: payload.source, | ||
| interactionType: payload.interactionType, | ||
| platform: payload.platform, | ||
| url: payload.url, | ||
| title: payload.title, | ||
| componentId: payload.componentId, | ||
| timestamp: payload.timestamp, | ||
| ...(payload.errorMessage ? { errorMessage: payload.errorMessage } : {}), | ||
| ...(payload.context ? { context: payload.context } : {}), | ||
| }); | ||
| } | ||
| } | ||
|
Comment on lines
+249
to
+267
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Add inline comment for Per coding guidelines for 📝 Suggested comment class GoogleTagManagerAdapter extends SocialShareAnalyticsPlugin {
+ // Pushes analytics event to GTM dataLayer; includes errorMessage/context only when present
track(payload) {
if (typeof window === "undefined" || !Array.isArray(window.dataLayer)) {
return;
}Based on learnings: "Flag any newly added or modified function that lacks a descriptive comment explaining what it does." 🤖 Prompt for AI Agents |
||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Console Adapter | ||
| // Lightweight adapter for development / quick debugging. | ||
| // ----------------------------------------------------------------------------- | ||
| class ConsoleAdapter extends SocialShareAnalyticsPlugin { | ||
| track(payload) { | ||
| // eslint-disable-next-line no-console | ||
| console.info("[SocialShareButton Analytics][ConsoleAdapter]", payload); | ||
| } | ||
| } | ||
|
|
||
| // ----------------------------------------------------------------------------- | ||
| // Custom / Callback Adapter | ||
| // Use this adapter to wrap any inline function without subclassing. | ||
|
|
@@ -278,6 +314,8 @@ const adapters = { | |
| SegmentAdapter, | ||
| PlausibleAdapter, | ||
| PostHogAdapter, | ||
| GoogleTagManagerAdapter, | ||
| ConsoleAdapter, | ||
| CustomAdapter, | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -38,6 +38,7 @@ class SocialShareButton { | |||||||||||||||||||||||||||||||||||||||
| // Analytics — the library emits events but never collects or sends data itself. | ||||||||||||||||||||||||||||||||||||||||
| // Website owners wire up their own analytics tools via these options. | ||||||||||||||||||||||||||||||||||||||||
| analytics: options.analytics !== false, // set to false to disable all event emission | ||||||||||||||||||||||||||||||||||||||||
| analyticsOptions: options.analyticsOptions || {}, // advanced analytics controls: sampleRate, filter, dedupeWindow, enrichContext | ||||||||||||||||||||||||||||||||||||||||
| onAnalytics: options.onAnalytics || null, // callback: (payload) => void | ||||||||||||||||||||||||||||||||||||||||
| analyticsPlugins: options.analyticsPlugins || [], // array of { track(payload) } adapters | ||||||||||||||||||||||||||||||||||||||||
| componentId: options.componentId || null, // optional identifier for this instance | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -58,6 +59,8 @@ class SocialShareButton { | |||||||||||||||||||||||||||||||||||||||
| this.ownsBodyLock = false; // Track if this instance owns the body overflow lock | ||||||||||||||||||||||||||||||||||||||||
| this.eventsAttached = false; // Guard against multiple attachEvents() calls | ||||||||||||||||||||||||||||||||||||||||
| this.isDestroyed = false; // Track if instance has been destroyed (prevents async callbacks) | ||||||||||||||||||||||||||||||||||||||||
| this._lastAnalyticsEvent = null; // For optional analytics dedupe by eventKey/time window | ||||||||||||||||||||||||||||||||||||||||
| this.analyticsHistory = []; // keep a local rolling history of emitted events | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (this.options.container) { | ||||||||||||||||||||||||||||||||||||||||
| this.init(); | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -689,6 +692,40 @@ class SocialShareButton { | |||||||||||||||||||||||||||||||||||||||
| : this.options.container; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Generates a short unique event ID for each emitted analytics event. | ||||||||||||||||||||||||||||||||||||||||
| * @returns {string} | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| _generateEventId() { | ||||||||||||||||||||||||||||||||||||||||
| return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+695
to
+701
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider adding brief inline comment explaining the ID format. Per coding guidelines, inline comments are preferred over JSDoc for this codebase. A short comment explaining the format would aid maintainability: 📝 Suggested enhancement _generateEventId() {
+ // Format: {base36-timestamp}-{random-8-chars} for lightweight client-side uniqueness
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
}As per coding guidelines: "Add comments for logical blocks explaining what they do." 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Update analytics options at runtime (includes dedupe settings, sampling). | ||||||||||||||||||||||||||||||||||||||||
| * @param {Object} opts | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| setAnalyticsOptions(opts = {}) { | ||||||||||||||||||||||||||||||||||||||||
| this.options.analyticsOptions = { | ||||||||||||||||||||||||||||||||||||||||
| ...this.options.analyticsOptions, | ||||||||||||||||||||||||||||||||||||||||
| ...opts, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Returns a read-only copy of the event history buffer. | ||||||||||||||||||||||||||||||||||||||||
| * @returns {Array<Object>} | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| getAnalyticsHistory() { | ||||||||||||||||||||||||||||||||||||||||
| return [...this.analyticsHistory]; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Clears tracked analytics event history. | ||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
| clearAnalyticsHistory() { | ||||||||||||||||||||||||||||||||||||||||
| this.analyticsHistory = []; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||||||
| * Logs analytics warnings only when debug mode is enabled. | ||||||||||||||||||||||||||||||||||||||||
| * @param {string} message - Description of the failed analytics path. | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -725,9 +762,12 @@ class SocialShareButton { | |||||||||||||||||||||||||||||||||||||||
| _emit(eventName, interactionType, extra = {}) { | ||||||||||||||||||||||||||||||||||||||||
| if (this.options.analytics === false) return; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const analyticsOptions = this.options.analyticsOptions || {}; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const payload = { | ||||||||||||||||||||||||||||||||||||||||
| version: ANALYTICS_SCHEMA_VERSION, | ||||||||||||||||||||||||||||||||||||||||
| source: "social-share-button", | ||||||||||||||||||||||||||||||||||||||||
| eventId: this._generateEventId(), | ||||||||||||||||||||||||||||||||||||||||
| eventName, | ||||||||||||||||||||||||||||||||||||||||
| interactionType, | ||||||||||||||||||||||||||||||||||||||||
| platform: extra.platform || null, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -741,6 +781,67 @@ class SocialShareButton { | |||||||||||||||||||||||||||||||||||||||
| payload.errorMessage = extra.errorMessage; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Analytics sample rate and filtering | ||||||||||||||||||||||||||||||||||||||||
| const sampleRate = Number(analyticsOptions.sampleRate || 1); | ||||||||||||||||||||||||||||||||||||||||
| if (sampleRate <= 0 || sampleRate > 0 && sampleRate < 1 && Math.random() > sampleRate) { | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (typeof analyticsOptions.filter === "function" && !analyticsOptions.filter(payload)) { | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+784
to
+792
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Add inline comment explaining sampling logic. The conditional on line 786 is correct but dense. An inline comment would clarify the edge cases (especially that values > 1 are treated as 100%): 📝 Suggested enhancement // Analytics sample rate and filtering
+ // sampleRate <= 0: disabled; 0 < sampleRate < 1: probabilistic; sampleRate >= 1: 100% pass-through
const sampleRate = Number(analyticsOptions.sampleRate || 1);
if (sampleRate <= 0 || sampleRate > 0 && sampleRate < 1 && Math.random() > sampleRate) {
return;
}As per coding guidelines: "Add comments for edge cases and non-obvious logic." 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Optional event dedupe within rolling window (milliseconds) | ||||||||||||||||||||||||||||||||||||||||
| const dedupeWindow = Number(analyticsOptions.dedupeWindow || 0); | ||||||||||||||||||||||||||||||||||||||||
| if (dedupeWindow > 0 && this._lastAnalyticsEvent) { | ||||||||||||||||||||||||||||||||||||||||
| let sameEvent = | ||||||||||||||||||||||||||||||||||||||||
| this._lastAnalyticsEvent.eventName === payload.eventName && | ||||||||||||||||||||||||||||||||||||||||
| this._lastAnalyticsEvent.platform === payload.platform && | ||||||||||||||||||||||||||||||||||||||||
| this._lastAnalyticsEvent.url === payload.url && | ||||||||||||||||||||||||||||||||||||||||
| this._lastAnalyticsEvent.interactionType === payload.interactionType; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (typeof analyticsOptions.dedupeBy === "function") { | ||||||||||||||||||||||||||||||||||||||||
| sameEvent = analyticsOptions.dedupeBy(this._lastAnalyticsEvent, payload); | ||||||||||||||||||||||||||||||||||||||||
| } else if (Array.isArray(analyticsOptions.dedupeBy)) { | ||||||||||||||||||||||||||||||||||||||||
| sameEvent = analyticsOptions.dedupeBy.every((key) => | ||||||||||||||||||||||||||||||||||||||||
| Object.prototype.hasOwnProperty.call(payload, key) | ||||||||||||||||||||||||||||||||||||||||
| ? this._lastAnalyticsEvent[key] === payload[key] | ||||||||||||||||||||||||||||||||||||||||
| : false | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+803
to
+811
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edge case: non-existent keys in If a developer specifies a key that doesn't exist in the payload (e.g., Consider either logging a debug warning for invalid keys, or skipping non-existent keys: 📝 Alternative approach that skips missing keys } else if (Array.isArray(analyticsOptions.dedupeBy)) {
- sameEvent = analyticsOptions.dedupeBy.every((key) =>
- Object.prototype.hasOwnProperty.call(payload, key)
- ? this._lastAnalyticsEvent[key] === payload[key]
- : false
- );
+ // Only compare keys that exist in both payloads
+ const validKeys = analyticsOptions.dedupeBy.filter(
+ (key) => Object.prototype.hasOwnProperty.call(payload, key)
+ );
+ sameEvent = validKeys.length > 0 && validKeys.every(
+ (key) => this._lastAnalyticsEvent[key] === payload[key]
+ );
}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (sameEvent && Date.now() - this._lastAnalyticsEvent.timestamp < dedupeWindow) { | ||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Enrich context if enabled (default true) | ||||||||||||||||||||||||||||||||||||||||
| if (analyticsOptions.enrichContext !== false) { | ||||||||||||||||||||||||||||||||||||||||
| payload.context = { | ||||||||||||||||||||||||||||||||||||||||
| referrer: typeof document !== "undefined" ? document.referrer || null : null, | ||||||||||||||||||||||||||||||||||||||||
| locale: | ||||||||||||||||||||||||||||||||||||||||
| typeof navigator !== "undefined" && navigator.language | ||||||||||||||||||||||||||||||||||||||||
| ? navigator.language | ||||||||||||||||||||||||||||||||||||||||
| : null, | ||||||||||||||||||||||||||||||||||||||||
| viewport: | ||||||||||||||||||||||||||||||||||||||||
| typeof window !== "undefined" | ||||||||||||||||||||||||||||||||||||||||
| ? `${window.innerWidth}x${window.innerHeight}` | ||||||||||||||||||||||||||||||||||||||||
| : null, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| if (analyticsOptions.includeUserAgent && typeof navigator !== "undefined") { | ||||||||||||||||||||||||||||||||||||||||
| payload.context.userAgent = navigator.userAgent; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| this._lastAnalyticsEvent = payload; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Keep rolling event history buffer for debugging/inspection | ||||||||||||||||||||||||||||||||||||||||
| const historyLimit = Number(analyticsOptions.historyLimit || 100); | ||||||||||||||||||||||||||||||||||||||||
| this.analyticsHistory.push(payload); | ||||||||||||||||||||||||||||||||||||||||
| if (this.analyticsHistory.length > historyLimit) { | ||||||||||||||||||||||||||||||||||||||||
| this.analyticsHistory.shift(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Optional console output for development / debugging | ||||||||||||||||||||||||||||||||||||||||
| if (this.options.debug) { | ||||||||||||||||||||||||||||||||||||||||
| // eslint-disable-next-line no-console | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example is incomplete —
ConsoleAdapterusage not explained.The code snippet uses
new ConsoleAdapter()directly, but doesn't show how to load or access it. Users following this example will get aReferenceError.Consider adding the required setup, similar to how it's documented in the analytics prompt file:
Alternatively, link to the detailed analytics integration documentation.
📝 Committable suggestion
🤖 Prompt for AI Agents